vpsnet-mcp 2.3.9 → 2.3.10
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 +41 -16
- package/build/application-contract.js +8 -4
- package/build/certificate-contract.js +18 -0
- package/build/custom-project-contract.js +16 -1
- package/build/index.js +371 -104
- package/build/networking-tools.js +87 -16
- package/build/remote-orders.js +1 -1
- package/build/remote-policy.js +16 -9
- package/build/temp-vm-contract.js +9 -10
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -6,14 +6,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { installRemotePolicy, remoteInstructions, orderInstructions } from "./remote-policy.js";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { registerNetworkingTools } from "./networking-tools.js";
|
|
12
12
|
import { isIP } from "node:net";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { apiRequest, formatJson, initializeStdioApi } from "./api.js";
|
|
15
15
|
import { installToolResultErrorFlag } from "./tool-result.js";
|
|
16
|
-
import { applicationAccessConfigurationRequestBody, applicationAccessSchema, applicationSingleAccessSchema,
|
|
16
|
+
import { applicationAccessConfigurationRequestBody, applicationAccessSchema, applicationSingleAccessSchema, applicationActionCancellationRequestBody, applicationActionIdSchema, applicationActionSchema, applicationDataRestoreIdSchema, applicationDataRestorePointIdSchema, applicationDataRestoreQuoteRequestBody, applicationDataRestoreQuoteTokenSchema, applicationDataRestoreRequestBody, applicationExpectedVersionSchema, applicationInstallRequestBody, applicationLifecycleRequestBody, applicationLogMaxBytesSchema, applicationLogServiceSchema, applicationLogTailLinesSchema, applicationRevisionSchema, applicationResourceCpuPercentSchema, applicationResourceEmailEnabledSchema, applicationResourceMemoryMiBSchema, applicationResourceNetworkMiBPerMinuteSchema, applicationResourceRestartDeltaSchema, applicationResourceThresholdRequestBody, safeApplicationInspectionPayload, safeApplicationDataRestorePayload, safeApplicationDataRestorePointsPayload, safeApplicationDataRestoreQuotePayload, safeApplicationMutationPayload, safeApplicationRegistryCredentialPayload, } from "./application-contract.js";
|
|
17
17
|
import { composeAdoptionIdSchema, composeProjectLabelSchema, customProjectComposeSchema, customProjectDefinitionRequestBody, customProjectEnvironmentSchema, customProjectIdSchema, customProjectNameSchema, customProjectRegistryCredentialIdsSchema, customProjectRevisionSchema, customProjectSecretNamesSchema, customProjectSecretsSchema, safeComposeAdoptionConfirmationPayload, safeComposeAdoptionPayload, safeContainerDiscoveryPayload, safeCustomProjectInstallPayload, safeCustomProjectPayload, safeCustomProjectReceiptPayload, safeCustomProjectValidationPayload, } from "./custom-project-contract.js";
|
|
18
18
|
import { certificateActionRequestBody, certificateActionRequestSchema, certificateAcmeSubscriptionActionRequestBody, certificateAcmeSubscriptionActionRequestSchema, certificateAcmeSubscriptionDomainInputShape, certificateAcmeSubscriptionInputShape, certificateOrderIdSchema, certificateOrderInputShape, certificateProductIdSchema, certificateSubscriptionIdSchema, freeCertificateActionRequestBody, freeCertificateActionRequestSchema, freeCertificateCreateInputShape, freeCertificateCreateRequestSchema, freeCertificatePreflightInputShape, freeCertificatePreflightRequestSchema, freeCertificateRequestIdSchema, freeCertificateStateSchema, safeCertificatePayload, } from "./certificate-contract.js";
|
|
19
19
|
import { fileBrowseDirectoryEntryIdSchema, fileBrowseFilterSchema, fileBrowseIdSchema, fileBrowseOffsetSchema, fileBrowsePointIdSchema, fileBrowseRequestBody, fileBrowseRequestRejection, readSearchAvailable, safeFileBrowsePayload, safeFileBrowsePointsPayload, } from "./file-restore-contract.js";
|
|
@@ -52,7 +52,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
52
52
|
"Flow: get_order_plans → get_order_options(plan) → order_service.",
|
|
53
53
|
"Payment object format: { payment: <numeric_id>, successUrl: '', cancelUrl: '' }.",
|
|
54
54
|
"For balance payment use payment ID 1: { payment: 1, successUrl: '', cancelUrl: '' }.",
|
|
55
|
-
"Paid API-key order tools require paid_operations_enabled, the matching paid scope (vps:order/vds:order/ds:order/fc:order), configured daily/monthly spend caps, and an idempotencyKey
|
|
55
|
+
"Paid API-key order tools require paid_operations_enabled, the matching paid scope (vps:order/vds:order/ds:order/fc:order), configured daily/monthly spend caps, and an idempotencyKey. First call order_service for a quote, show its exact gross EUR amount and billing period, then call it again with the unchanged configuration, key, returned quoteToken, expected_total_eur and acknowledge_charge=true only after explicit approval.",
|
|
56
56
|
"Resources is an array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
|
|
57
57
|
"rootPassword rules: 6-40 chars, alphanumeric only, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'.",
|
|
58
58
|
"sshKey and rootPassword are mutually exclusive — provide one or the other (or neither for auto-generated password).",
|
|
@@ -150,7 +150,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
150
150
|
"Automatic SSL flow: list_certificate_catalog → quote_automatic_ssl_subscription → order_automatic_ssl_subscription. It works with compatible ACME clients on VPSnet or another provider. A base domain includes its www alias; apex and wildcard names are separate subscribed identifiers and may be ordered together. Choose auto_renew explicitly; every next term is still quoted and prepaid from balance during the final 30 days.",
|
|
151
151
|
"After payment, poll get_automatic_ssl_subscription until state=active and credentials_available=true. The private ACME server URL and EAB credentials are intentionally portal-only behind two-factor verification; they are not MCP or API-key tools and must never enter model context.",
|
|
152
152
|
"Add a DNS name with quote_automatic_ssl_domain → order_automatic_ssl_domain using the exact quote token and unchanged idempotency key. The quote prevents duplicate coverage from base/www and wildcard/base relationships.",
|
|
153
|
-
"When renewal.available=true, use quote_automatic_ssl_renewal → order_automatic_ssl_renewal. Renewal is balance-only and must be explicitly approved; payment_processing or
|
|
153
|
+
"When renewal.available=true, use quote_automatic_ssl_renewal → order_automatic_ssl_renewal. Renewal is balance-only and must be explicitly approved; payment_processing=true or state=processing means completion is still pending; poll the subscription and its current renewal before reporting success.",
|
|
154
154
|
"Use list_automatic_ssl_actions before cancel_automatic_ssl_subscription, remove_automatic_ssl_domain or replace_automatic_ssl_domain. Cancellation, removal, and same-type correction are durable and idempotent. Reuse a key only for the exact same request. For reconciliation_required or outcome_ambiguous, call refresh_automatic_ssl_subscription and read state instead of creating a new mutation.",
|
|
155
155
|
"Flow: list_certificate_catalog → quote_certificate → order_certificate. The quote returns the final customer price in EUR including VAT, a short-lived quote token, and the exact public-key type derived from the CSR. Disclose the exact total and obtain explicit approval before ordering.",
|
|
156
156
|
"The customer creates and retains the private key. Certificate tools accept only a signed public PKCS#10 CSR. Never ask for, accept, store, repeat, or place a private key in a tool argument or model context.",
|
|
@@ -415,9 +415,10 @@ export function createVpsnetServer(options = {}) {
|
|
|
415
415
|
description: "Get action history for a service Requires services:read when called with an API key.",
|
|
416
416
|
inputSchema: {
|
|
417
417
|
orderNo: serviceOrderNoSchema,
|
|
418
|
+
page: z.number().int().min(1).max(1000000).optional().describe("Page number; follow response pagination for older records."),
|
|
418
419
|
},
|
|
419
|
-
}, async ({ orderNo }) => {
|
|
420
|
-
const { data } = await apiRequest("GET",
|
|
420
|
+
}, async ({ orderNo, page }) => {
|
|
421
|
+
const { data } = await apiRequest("GET", svc(orderNo, "history") + (page === undefined ? "" : `?page=${page}`));
|
|
421
422
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
422
423
|
});
|
|
423
424
|
server.registerTool("get_service_rescue", {
|
|
@@ -737,6 +738,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
737
738
|
validation: {
|
|
738
739
|
...latestValidation,
|
|
739
740
|
source_compose_digest: createdValidation.source_compose_digest,
|
|
741
|
+
compose_digest: latestValidation.compose_digest ?? createdValidation.compose_digest,
|
|
740
742
|
resolved_compose_yaml: createdValidation.resolved_compose_yaml,
|
|
741
743
|
image_resolutions: createdValidation.image_resolutions,
|
|
742
744
|
},
|
|
@@ -747,7 +749,8 @@ export function createVpsnetServer(options = {}) {
|
|
|
747
749
|
const validation = applicationAsyncEnvelope(data, "validation");
|
|
748
750
|
if (validation.state !== "succeeded")
|
|
749
751
|
return false;
|
|
750
|
-
if (!data || typeof data !== "object" || Array.isArray(data)
|
|
752
|
+
if (!data || typeof data !== "object" || Array.isArray(data)
|
|
753
|
+
|| data.success !== true)
|
|
751
754
|
return false;
|
|
752
755
|
const value = data.validation;
|
|
753
756
|
return Boolean(value
|
|
@@ -769,8 +772,95 @@ export function createVpsnetServer(options = {}) {
|
|
|
769
772
|
? compose
|
|
770
773
|
: null;
|
|
771
774
|
}
|
|
775
|
+
function applicationPollBindingMatches(data, key, id, installationId, kind) {
|
|
776
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
777
|
+
return false;
|
|
778
|
+
const value = data[key];
|
|
779
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
780
|
+
return false;
|
|
781
|
+
const operation = value;
|
|
782
|
+
return operation.id === id
|
|
783
|
+
&& (installationId === undefined || operation.installation_id === installationId)
|
|
784
|
+
&& (kind === undefined || operation.kind === kind);
|
|
785
|
+
}
|
|
786
|
+
function applicationPollBindingFailure() {
|
|
787
|
+
return {
|
|
788
|
+
isError: true,
|
|
789
|
+
content: [{ type: "text", text: formatJson({
|
|
790
|
+
success: false,
|
|
791
|
+
error_codes: ["applicationOperationBindingMismatch"],
|
|
792
|
+
}) }],
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
server.registerTool("get_application_inspection", {
|
|
796
|
+
description: "Poll the exact persisted health or logs inspection returned by get_application_health or get_application_logs when it remains pending. This GET does not queue another inspection. Keep the same service, installation, inspection UUID and kind until succeeded, failed or expired. Requires applications:read.",
|
|
797
|
+
inputSchema: {
|
|
798
|
+
orderNo: applicationOrderNoSchema,
|
|
799
|
+
installation_id: applicationInstallationIdSchema,
|
|
800
|
+
inspection_id: z.string().uuid(),
|
|
801
|
+
kind: z.enum(["health", "logs"]),
|
|
802
|
+
},
|
|
803
|
+
annotations: { title: "Get application inspection", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
804
|
+
}, async ({ orderNo, installation_id, inspection_id, kind }) => {
|
|
805
|
+
const { status, data } = await apiRequest("GET", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}/inspections/${encodeURIComponent(inspection_id)}`));
|
|
806
|
+
if (status >= 200 && status < 300
|
|
807
|
+
&& !applicationPollBindingMatches(data, "inspection", inspection_id, installation_id, kind)) {
|
|
808
|
+
return applicationPollBindingFailure();
|
|
809
|
+
}
|
|
810
|
+
return { content: [{ type: "text", text: safeApplicationInspectionResult(status, data, kind) }] };
|
|
811
|
+
});
|
|
812
|
+
server.registerTool("get_application_recipe_validation", {
|
|
813
|
+
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.",
|
|
814
|
+
inputSchema: { orderNo: applicationOrderNoSchema, validation_id: z.string().uuid() },
|
|
815
|
+
annotations: { title: "Get application recipe validation", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
816
|
+
}, async ({ orderNo, validation_id }) => {
|
|
817
|
+
const { status, data } = await apiRequest("GET", applicationPath(orderNo, `custom-projects/validations/${encodeURIComponent(validation_id)}`));
|
|
818
|
+
if (status >= 200 && status < 300 && !applicationPollBindingMatches(data, "validation", validation_id)) {
|
|
819
|
+
return applicationPollBindingFailure();
|
|
820
|
+
}
|
|
821
|
+
return { content: [{ type: "text", text: formatJson(safeCustomProjectValidationPayload(status, data)) }] };
|
|
822
|
+
});
|
|
823
|
+
server.registerTool("get_service_container_discovery", {
|
|
824
|
+
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.",
|
|
825
|
+
inputSchema: { orderNo: applicationOrderNoSchema, discovery_id: z.string().uuid() },
|
|
826
|
+
annotations: { title: "Get service container discovery", readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
827
|
+
}, async ({ orderNo, discovery_id }) => {
|
|
828
|
+
const { status, data } = await apiRequest("GET", applicationPath(orderNo, `container-discoveries/${encodeURIComponent(discovery_id)}`));
|
|
829
|
+
if (status >= 200 && status < 300 && !applicationPollBindingMatches(data, "discovery", discovery_id)) {
|
|
830
|
+
return applicationPollBindingFailure();
|
|
831
|
+
}
|
|
832
|
+
return { content: [{ type: "text", text: formatJson(safeContainerDiscoveryPayload(status, data)) }] };
|
|
833
|
+
});
|
|
834
|
+
async function validateApplicationRecipeForSave(orderNo, composeYaml, registryCredentialIds, validationId) {
|
|
835
|
+
const basePath = applicationPath(orderNo, "custom-projects");
|
|
836
|
+
if (!validationId) {
|
|
837
|
+
return runApplicationAsyncOperation(`${basePath}/validate`, id => `${basePath}/validations/${id}`, "validation", { compose_yaml: composeYaml, registry_credential_ids: registryCredentialIds });
|
|
838
|
+
}
|
|
839
|
+
const result = await apiRequest("GET", `${basePath}/validations/${encodeURIComponent(validationId)}`);
|
|
840
|
+
if (result.status < 200 || result.status >= 300)
|
|
841
|
+
return result;
|
|
842
|
+
const validation = result.data?.validation;
|
|
843
|
+
if (!applicationPollBindingMatches(result.data, "validation", validationId)
|
|
844
|
+
|| validation?.compose_digest !== createHash("sha256").update(composeYaml, "utf8").digest("hex")) {
|
|
845
|
+
return { status: 409, data: { applicationOperationBindingMismatch: true } };
|
|
846
|
+
}
|
|
847
|
+
const expiresAt = validation?.timestamps?.expires_at;
|
|
848
|
+
const expiry = typeof expiresAt === "string"
|
|
849
|
+
? Date.parse(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(expiresAt)
|
|
850
|
+
? expiresAt.replace(" ", "T") + "Z" : expiresAt)
|
|
851
|
+
: NaN;
|
|
852
|
+
if (!Number.isFinite(expiry) || expiry <= Date.now()) {
|
|
853
|
+
return { status: 409, data: { applicationCustomProjectValidationExpired: true } };
|
|
854
|
+
}
|
|
855
|
+
// The backend independently captures valid, unexpired evidence for this
|
|
856
|
+
// exact digest and enforces the submitted service's registry ownership.
|
|
857
|
+
return { status: result.status, data: {
|
|
858
|
+
...result.data,
|
|
859
|
+
validation: { ...validation, resolved_compose_yaml: composeYaml },
|
|
860
|
+
} };
|
|
861
|
+
}
|
|
772
862
|
server.registerTool("get_application_health", {
|
|
773
|
-
description: "Run an on-demand health inspection of one owned managed application and return the observed container/service health. Use this instead of trusting the last-reported health, which can be stale. This queues an inspection (a POST), so it needs an API key that permits write operations; a read-only key is rejected. Requires applications:read.",
|
|
863
|
+
description: "Run an on-demand health inspection of one owned managed application and return the observed container/service health. Use this instead of trusting the last-reported health, which can be stale. If still pending after polling, retain inspection.id and call get_application_inspection with kind=health. This queues an inspection (a POST), so it needs an API key that permits write operations; a read-only key is rejected. Requires applications:read.",
|
|
774
864
|
inputSchema: {
|
|
775
865
|
orderNo: applicationOrderNoSchema,
|
|
776
866
|
installation_id: applicationInstallationIdSchema,
|
|
@@ -788,7 +878,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
788
878
|
};
|
|
789
879
|
});
|
|
790
880
|
server.registerTool("get_application_logs", {
|
|
791
|
-
description: "Run an on-demand logs inspection of one owned managed application and return recent, size-bounded container logs for troubleshooting. Optionally select one exact Compose service; omit it to inspect all services. This queues an inspection (a POST), so it needs an API key that permits write operations; a read-only key is rejected. Requires applications:read.",
|
|
881
|
+
description: "Run an on-demand logs inspection of one owned managed application and return recent, size-bounded container logs for troubleshooting. If still pending after polling, retain inspection.id and call get_application_inspection with kind=logs. Optionally select one exact Compose service; omit it to inspect all services. This queues an inspection (a POST), so it needs an API key that permits write operations; a read-only key is rejected. Requires applications:read.",
|
|
792
882
|
inputSchema: {
|
|
793
883
|
orderNo: applicationOrderNoSchema,
|
|
794
884
|
installation_id: applicationInstallationIdSchema,
|
|
@@ -926,7 +1016,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
926
1016
|
};
|
|
927
1017
|
});
|
|
928
1018
|
server.registerTool("validate_application_recipe", {
|
|
929
|
-
description: "Resolve image tags from their registries to immutable digests, then validate the resulting customer-owned Docker Compose document with the authoritative policy on the target Firecracker worker. The successful result includes the exact digest-pinned definition to store. This does not create, install, or modify a project or container. The queued validation is polled for about 30 seconds and may still return pending. Requires applications:manage.",
|
|
1019
|
+
description: "Resolve image tags from their registries to immutable digests, then validate the resulting customer-owned Docker Compose document with the authoritative policy on the target Firecracker worker. The successful result includes the exact digest-pinned definition to store. This does not create, install, or modify a project or container. The queued validation is polled for about 30 seconds and may still return pending. Retain its ID and resolved_compose_yaml, poll get_application_recipe_validation, and resume recipe creation with the same pinned document and validation_id after valid=true. Requires applications:manage.",
|
|
930
1020
|
inputSchema: {
|
|
931
1021
|
orderNo: applicationOrderNoSchema,
|
|
932
1022
|
compose_yaml: customProjectComposeSchema,
|
|
@@ -991,11 +1081,12 @@ export function createVpsnetServer(options = {}) {
|
|
|
991
1081
|
};
|
|
992
1082
|
});
|
|
993
1083
|
server.registerTool("create_application_recipe", {
|
|
994
|
-
description: "Resolve image tags from their registries, validate the resulting digest-pinned Compose definition, and freeze that exact definition as the first immutable customer-owned recipe revision. This saves a definition only and does not install or start containers. Plain environment values belong in env; secret_names contain names only. Confirm the exact service, name, Compose definition, variable names, and registry bindings first. Requires applications:manage.",
|
|
1084
|
+
description: "Resolve image tags from their registries, validate the resulting digest-pinned Compose definition, and freeze that exact definition as the first immutable customer-owned recipe revision. This saves a definition only and does not install or start containers. If validation remains pending, retain its ID and resolved_compose_yaml, poll get_application_recipe_validation, then retry with validation_id and that exact pinned document. Plain environment values belong in env; secret_names contain names only. Confirm the exact service, name, Compose definition, variable names, and registry bindings first. Requires applications:manage.",
|
|
995
1085
|
inputSchema: {
|
|
996
1086
|
orderNo: applicationOrderNoSchema,
|
|
997
1087
|
name: customProjectNameSchema,
|
|
998
1088
|
compose_yaml: customProjectComposeSchema,
|
|
1089
|
+
validation_id: z.string().uuid().optional().describe("Resume only the exact successful validation for this service and digest-pinned compose_yaml; retain the initial resolved document"),
|
|
999
1090
|
env: customProjectEnvironmentSchema,
|
|
1000
1091
|
secret_names: customProjectSecretNamesSchema,
|
|
1001
1092
|
registry_credential_ids: customProjectRegistryCredentialIdsSchema,
|
|
@@ -1009,13 +1100,13 @@ export function createVpsnetServer(options = {}) {
|
|
|
1009
1100
|
idempotentHint: true,
|
|
1010
1101
|
openWorldHint: true,
|
|
1011
1102
|
},
|
|
1012
|
-
}, async ({ orderNo, name, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1103
|
+
}, async ({ orderNo, name, compose_yaml, validation_id, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1013
1104
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
1014
|
-
const validation = await
|
|
1105
|
+
const validation = await validateApplicationRecipeForSave(orderNo, compose_yaml, registry_credential_ids, validation_id);
|
|
1015
1106
|
if (validation.status >= 400
|
|
1016
1107
|
|| !applicationRecipeValidationPassed(validation.data)) {
|
|
1017
1108
|
return {
|
|
1018
|
-
isError:
|
|
1109
|
+
isError: validation.status >= 400 || !["queued", "awaiting_reply"].includes(String(applicationAsyncEnvelope(validation.data, "validation").state)),
|
|
1019
1110
|
content: [{
|
|
1020
1111
|
type: "text",
|
|
1021
1112
|
text: formatJson(safeCustomProjectValidationPayload(validation.status, validation.data)),
|
|
@@ -1049,11 +1140,12 @@ export function createVpsnetServer(options = {}) {
|
|
|
1049
1140
|
};
|
|
1050
1141
|
});
|
|
1051
1142
|
server.registerTool("create_application_recipe_revision", {
|
|
1052
|
-
description: "Resolve image tags from their registries, validate the resulting digest-pinned Compose definition, and freeze that exact definition as a later immutable customer-owned recipe revision. This does not update the running installation. In-place updates retain the existing secret names and registry bindings for rollback safety; create a separate project when those bindings must change. Requires applications:manage.",
|
|
1143
|
+
description: "Resolve image tags from their registries, validate the resulting digest-pinned Compose definition, and freeze that exact definition as a later immutable customer-owned recipe revision. This does not update the running installation. If validation remains pending, retain its ID and resolved_compose_yaml, poll get_application_recipe_validation, then retry with validation_id and that exact pinned document. In-place updates retain the existing secret names and registry bindings for rollback safety; create a separate project when those bindings must change. Requires applications:manage.",
|
|
1053
1144
|
inputSchema: {
|
|
1054
1145
|
orderNo: applicationOrderNoSchema,
|
|
1055
1146
|
project_id: customProjectIdSchema,
|
|
1056
1147
|
compose_yaml: customProjectComposeSchema,
|
|
1148
|
+
validation_id: z.string().uuid().optional().describe("Resume only the exact successful validation for this service and digest-pinned compose_yaml; retain the initial resolved document"),
|
|
1057
1149
|
env: customProjectEnvironmentSchema,
|
|
1058
1150
|
secret_names: customProjectSecretNamesSchema,
|
|
1059
1151
|
registry_credential_ids: customProjectRegistryCredentialIdsSchema,
|
|
@@ -1067,13 +1159,13 @@ export function createVpsnetServer(options = {}) {
|
|
|
1067
1159
|
idempotentHint: true,
|
|
1068
1160
|
openWorldHint: true,
|
|
1069
1161
|
},
|
|
1070
|
-
}, async ({ orderNo, project_id, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1162
|
+
}, async ({ orderNo, project_id, compose_yaml, validation_id, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1071
1163
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
1072
|
-
const validation = await
|
|
1164
|
+
const validation = await validateApplicationRecipeForSave(orderNo, compose_yaml, registry_credential_ids, validation_id);
|
|
1073
1165
|
if (validation.status >= 400
|
|
1074
1166
|
|| !applicationRecipeValidationPassed(validation.data)) {
|
|
1075
1167
|
return {
|
|
1076
|
-
isError:
|
|
1168
|
+
isError: validation.status >= 400 || !["queued", "awaiting_reply"].includes(String(applicationAsyncEnvelope(validation.data, "validation").state)),
|
|
1077
1169
|
content: [{
|
|
1078
1170
|
type: "text",
|
|
1079
1171
|
text: formatJson(safeCustomProjectValidationPayload(validation.status, validation.data)),
|
|
@@ -1161,7 +1253,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1161
1253
|
};
|
|
1162
1254
|
});
|
|
1163
1255
|
server.registerTool("discover_service_containers", {
|
|
1164
|
-
description: "Run a bounded read-only Docker discovery on one supported Firecracker service. Returns container names, images, state, health, published ports, Compose labels, and whether each container is VPSnet-managed. It never returns environment values or mounts and does not adopt or modify containers. Requires applications:read.",
|
|
1256
|
+
description: "Run a bounded read-only Docker discovery on one supported Firecracker service. Returns container names, images, state, health, published ports, Compose labels, and whether each container is VPSnet-managed. If still pending after polling, retain discovery.id and call get_service_container_discovery. It never returns environment values or mounts and does not adopt or modify containers. Requires applications:read.",
|
|
1165
1257
|
inputSchema: {
|
|
1166
1258
|
orderNo: applicationOrderNoSchema,
|
|
1167
1259
|
},
|
|
@@ -1350,30 +1442,20 @@ export function createVpsnetServer(options = {}) {
|
|
|
1350
1442
|
}) }],
|
|
1351
1443
|
};
|
|
1352
1444
|
}
|
|
1353
|
-
if (action === "update") {
|
|
1445
|
+
if (action === "update" || action === "rollback") {
|
|
1354
1446
|
if (!expected_blueprint_version || !expected_upstream_version) {
|
|
1355
1447
|
return {
|
|
1356
1448
|
isError: true,
|
|
1357
1449
|
content: [{ type: "text", text: formatJson({
|
|
1358
1450
|
success: false,
|
|
1359
|
-
error_codes: ["
|
|
1360
|
-
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
const current = await apiRequest("GET", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}`));
|
|
1364
|
-
if (current.status < 200 || current.status >= 300
|
|
1365
|
-
|| !applicationUpdateCandidateMatches(current.data, {
|
|
1366
|
-
blueprintVersion: expected_blueprint_version,
|
|
1367
|
-
upstreamVersion: expected_upstream_version,
|
|
1368
|
-
})) {
|
|
1369
|
-
return {
|
|
1370
|
-
isError: true,
|
|
1371
|
-
content: [{ type: "text", text: formatJson({
|
|
1372
|
-
success: false,
|
|
1373
|
-
error_codes: ["applicationUpdateExpectationChanged"],
|
|
1451
|
+
error_codes: [action === "rollback"
|
|
1452
|
+
? "applicationRollbackExpectationRequired" : "applicationUpdateExpectationRequired"],
|
|
1374
1453
|
}) }],
|
|
1375
1454
|
};
|
|
1376
1455
|
}
|
|
1456
|
+
// The backend checks replay before current eligibility and atomically
|
|
1457
|
+
// verifies these exact versions for a new key. A pre-read here would block
|
|
1458
|
+
// legitimate retries once the original transition changes current state.
|
|
1377
1459
|
}
|
|
1378
1460
|
else if (expected_blueprint_version || expected_upstream_version) {
|
|
1379
1461
|
return {
|
|
@@ -1384,13 +1466,13 @@ export function createVpsnetServer(options = {}) {
|
|
|
1384
1466
|
}) }],
|
|
1385
1467
|
};
|
|
1386
1468
|
}
|
|
1387
|
-
const { status, data } = await apiRequest("POST", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}/actions`), applicationLifecycleRequestBody(action, acknowledge_data_loss === true, action === "update"
|
|
1469
|
+
const { status, data } = await apiRequest("POST", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}/actions`), applicationLifecycleRequestBody(action, acknowledge_data_loss === true, (action === "update" || action === "rollback")
|
|
1388
1470
|
? { blueprintVersion: expected_blueprint_version, upstreamVersion: expected_upstream_version }
|
|
1389
1471
|
: undefined), { "Idempotency-Key": idempotencyKey });
|
|
1390
1472
|
return { content: [{ type: "text", text: safeApplicationMutationResult(status, data, orderNo) }] };
|
|
1391
1473
|
}
|
|
1392
1474
|
server.registerTool("manage_application", {
|
|
1393
|
-
description: "Queue one supported lifecycle action for an owned managed application. Confirm the exact action with the user first. For update, first read get_application_installation and copy the exact advertised blueprint_version and upstream_version as execution preconditions. The backend still selects and freezes the eligible immutable release; callers cannot choose an arbitrary target. Stop interrupts service. Uninstall permanently deletes the managed containers, configuration, saved credentials, and application data; existing server backups are retained. Uninstall requires acknowledge_data_loss=true after explicit user confirmation. A queued response must be verified with get_application_installation and get_application_events. Selective data restore is a separate operation exposed by list_application_restore_points and restore_application_data. Requires applications:manage and is not a paid API-key operation.",
|
|
1475
|
+
description: "Queue one supported lifecycle action for an owned managed application. Confirm the exact action with the user first. For update or rollback, first read get_application_installation and copy the exact advertised blueprint_version and upstream_version as execution preconditions. The backend still selects and freezes the eligible immutable release; callers cannot choose an arbitrary target. Stop interrupts service. Uninstall permanently deletes the managed containers, configuration, saved credentials, and application data; existing server backups are retained. Uninstall requires acknowledge_data_loss=true after explicit user confirmation. A queued response must be verified with get_application_installation and get_application_events. Selective data restore is a separate operation exposed by list_application_restore_points and restore_application_data. Requires applications:manage and is not a paid API-key operation.",
|
|
1394
1476
|
inputSchema: {
|
|
1395
1477
|
orderNo: applicationOrderNoSchema,
|
|
1396
1478
|
installation_id: applicationInstallationIdSchema,
|
|
@@ -1398,10 +1480,10 @@ export function createVpsnetServer(options = {}) {
|
|
|
1398
1480
|
idempotencyKey: idempotencyKeySchema.describe("Client-global unique key. Reuse it only to replay this exact lifecycle request; never reuse it for another service or request."),
|
|
1399
1481
|
expected_blueprint_version: applicationExpectedVersionSchema
|
|
1400
1482
|
.optional()
|
|
1401
|
-
.describe("Required for update: exact blueprint_version from the
|
|
1483
|
+
.describe("Required for update or rollback: exact blueprint_version from the corresponding available_actions capability"),
|
|
1402
1484
|
expected_upstream_version: applicationExpectedVersionSchema
|
|
1403
1485
|
.optional()
|
|
1404
|
-
.describe("Required for update: exact upstream_version from the
|
|
1486
|
+
.describe("Required for update or rollback: exact upstream_version from the corresponding available_actions capability"),
|
|
1405
1487
|
confirmed: z
|
|
1406
1488
|
.literal(true)
|
|
1407
1489
|
.describe("True only after the user confirmed this lifecycle action"),
|
|
@@ -1471,8 +1553,17 @@ export function createVpsnetServer(options = {}) {
|
|
|
1471
1553
|
},
|
|
1472
1554
|
annotations: { title: "Update application", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
1473
1555
|
}, async (input) => performApplicationLifecycleAction({ ...input, action: "update" }));
|
|
1556
|
+
server.registerTool("rollback_application", {
|
|
1557
|
+
description: "Queue the exact reverse transition advertised as type=rollback by get_application_installation. Read its release and backup, disclose replacement of current application data by that backup, and obtain explicit approval. Copy its blueprint_version and upstream_version as execution preconditions; arbitrary targets are refused. Poll installation and events for the final result. Reuse the same idempotency key and exact versions for an unchanged retry even after availability changes. Requires applications:manage.",
|
|
1558
|
+
inputSchema: {
|
|
1559
|
+
...applicationLifecycleBaseShape,
|
|
1560
|
+
expected_blueprint_version: applicationExpectedVersionSchema.describe("Exact blueprint_version from the advertised rollback release"),
|
|
1561
|
+
expected_upstream_version: applicationExpectedVersionSchema.describe("Exact upstream_version from the advertised rollback release"),
|
|
1562
|
+
},
|
|
1563
|
+
annotations: { title: "Roll back application", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
1564
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "rollback" }));
|
|
1474
1565
|
server.registerTool("cancel_application_action", {
|
|
1475
|
-
description: "Request pre-dispatch cancellation of the exact latest action for one owned managed application. This is available only while get_application_installation advertises latest_action.cancellable=true for the same action UUID. The
|
|
1566
|
+
description: "Request pre-dispatch cancellation of the exact latest action for one owned managed application. This is available only while get_application_installation advertises latest_action.cancellable=true for the same action UUID. The backend verifies eligibility for new requests and replays the exact same-key retry even after the advertised state changes. Cancellation never stops or interrupts a running worker job. Requires applications:manage, the exact latest_action.id UUID, and a fresh client-global idempotencyKey that was not used for the original action or another request. Reuse that key only to replay this exact cancellation request. This is not a paid API-key operation.",
|
|
1476
1567
|
inputSchema: {
|
|
1477
1568
|
orderNo: applicationOrderNoSchema,
|
|
1478
1569
|
installation_id: applicationInstallationIdSchema,
|
|
@@ -1487,23 +1578,9 @@ export function createVpsnetServer(options = {}) {
|
|
|
1487
1578
|
},
|
|
1488
1579
|
}, async ({ orderNo, installation_id, action_id, idempotencyKey }) => {
|
|
1489
1580
|
const installationPath = applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}`);
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
!applicationActionCancellationIsAdvertised(current.data, action_id)) {
|
|
1494
|
-
return {
|
|
1495
|
-
isError: true,
|
|
1496
|
-
content: [
|
|
1497
|
-
{
|
|
1498
|
-
type: "text",
|
|
1499
|
-
text: formatJson({
|
|
1500
|
-
success: false,
|
|
1501
|
-
error_codes: ["applicationActionCancellationNotAdvertised"],
|
|
1502
|
-
}),
|
|
1503
|
-
},
|
|
1504
|
-
],
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1581
|
+
// Cancellation eligibility is enforced by the backend after its exact
|
|
1582
|
+
// idempotency replay lookup. Do not reject a successful retry merely
|
|
1583
|
+
// because the action is now cancelled or no longer latest.
|
|
1507
1584
|
const { status, data } = await apiRequest("POST", `${installationPath}/actions/${encodeURIComponent(action_id)}/cancel`, applicationActionCancellationRequestBody(), { "Idempotency-Key": idempotencyKey });
|
|
1508
1585
|
return {
|
|
1509
1586
|
content: [
|
|
@@ -1543,7 +1620,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1543
1620
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1544
1621
|
});
|
|
1545
1622
|
server.registerTool("console_service", {
|
|
1546
|
-
description: "
|
|
1623
|
+
description: "Request a legacy or Cloud VPS console session and return its tracking event ID. This tool cannot redeem the one-time console token: redemption requires the customer signed into the VPSnet panel. Direct the user to the service console in the panel to open it. Firecracker serial console is browser-only and refuses API-key calls. The service must be running unless an active rescue session permits console access. Requires services:manage and a full-access API key.",
|
|
1547
1624
|
inputSchema: {
|
|
1548
1625
|
orderNo: serviceOrderNoSchema,
|
|
1549
1626
|
},
|
|
@@ -1610,6 +1687,39 @@ export function createVpsnetServer(options = {}) {
|
|
|
1610
1687
|
const { data } = await apiRequest("POST", svc(orderNo, "change-root-password"), { password });
|
|
1611
1688
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1612
1689
|
});
|
|
1690
|
+
// Guest resolver configuration is separate from registrar nameservers and
|
|
1691
|
+
// authoritative DNS records. The backend retains product, agent and lock guards.
|
|
1692
|
+
server.registerTool("get_guest_nameservers", {
|
|
1693
|
+
description: "Get the server's saved guest DNS resolver configuration: nsData.type is own (VPSnet defaults) or custom, with ns1-ns4 resolver IP addresses. Own mode stores null custom addresses; these are not an observation of the guest's current resolver file. Requires services:read.",
|
|
1694
|
+
inputSchema: { orderNo: serviceOrderNoSchema },
|
|
1695
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1696
|
+
}, async ({ orderNo }) => {
|
|
1697
|
+
const { data } = await apiRequest("GET", svc(orderNo, "change-ns"));
|
|
1698
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1699
|
+
});
|
|
1700
|
+
server.registerTool("set_guest_nameservers", {
|
|
1701
|
+
description: "Change DNS resolvers inside a supported Linux server. Read get_guest_nameservers first and confirm the exact service and resolver change; an unreachable resolver can interrupt guest internet connectivity. type=own restores the service's VPSnet defaults and clears saved custom addresses (omit ns1-ns4). type=custom requires ns1 and ns2, with optional ns3 and ns4; each is an IPv4 or IPv6 address, never a hostname. Returns unchanged=true when already configured, otherwise a noty UUID: follow wait_for_event, then verify the saved configuration and connectivity. This route is not idempotency-keyed; do not automatically replay an uncertain change. Requires services:manage and a full-access API key.",
|
|
1702
|
+
inputSchema: {
|
|
1703
|
+
orderNo: serviceOrderNoSchema,
|
|
1704
|
+
type: z.enum(["own", "custom"]).describe("own restores VPSnet default resolvers; custom replaces the saved resolver list"),
|
|
1705
|
+
ns1: serviceIpSchema.optional().describe("First resolver IPv4 or IPv6 address; required for custom mode"),
|
|
1706
|
+
ns2: serviceIpSchema.optional().describe("Second resolver IPv4 or IPv6 address; required for custom mode"),
|
|
1707
|
+
ns3: serviceIpSchema.optional().describe("Optional third resolver IPv4 or IPv6 address"),
|
|
1708
|
+
ns4: serviceIpSchema.optional().describe("Optional fourth resolver IPv4 or IPv6 address"),
|
|
1709
|
+
},
|
|
1710
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
1711
|
+
}, async ({ orderNo, type, ns1, ns2, ns3, ns4 }) => {
|
|
1712
|
+
if (type === "custom" && (ns1 === undefined || ns2 === undefined)) {
|
|
1713
|
+
throw new Error("Custom guest nameservers require both ns1 and ns2 resolver IP addresses.");
|
|
1714
|
+
}
|
|
1715
|
+
const body = { type };
|
|
1716
|
+
for (const [name, value] of Object.entries({ ns1, ns2, ns3, ns4 })) {
|
|
1717
|
+
if (value !== undefined)
|
|
1718
|
+
body[name] = value;
|
|
1719
|
+
}
|
|
1720
|
+
const { data } = await apiRequest("POST", svc(orderNo, "change-ns"), body);
|
|
1721
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1722
|
+
});
|
|
1613
1723
|
server.registerTool("get_rdns", {
|
|
1614
1724
|
description: "Get effective PTR records for every assigned IPv4 and enabled IPv6 address. The response identifies the automatic default and whether each value is a customer override. Read-only API keys are accepted. Requires services:read when called with an API key.",
|
|
1615
1725
|
inputSchema: {
|
|
@@ -1719,9 +1829,11 @@ export function createVpsnetServer(options = {}) {
|
|
|
1719
1829
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1720
1830
|
});
|
|
1721
1831
|
server.registerTool("reinstall_os", {
|
|
1722
|
-
description: "Reinstall OS on VPS. 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.
|
|
1832
|
+
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.",
|
|
1723
1833
|
inputSchema: {
|
|
1724
1834
|
orderNo: serviceOrderNoSchema,
|
|
1835
|
+
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."),
|
|
1836
|
+
sshKey: positiveIdSchema.optional().describe("Dedicated only: account SSH key ID from list_ssh_keys; omitted reuses the installed key."),
|
|
1725
1837
|
osVersion: z
|
|
1726
1838
|
.number()
|
|
1727
1839
|
.describe("OS version ID from get_os_options"),
|
|
@@ -1739,8 +1851,25 @@ export function createVpsnetServer(options = {}) {
|
|
|
1739
1851
|
destructiveHint: true,
|
|
1740
1852
|
idempotentHint: false,
|
|
1741
1853
|
},
|
|
1742
|
-
}, async ({ orderNo, osVersion, rootPassword, confirmSnapshotDelete }) => {
|
|
1854
|
+
}, async ({ orderNo, osVersion, rootPassword, confirmSnapshotDelete, confirmReinstall, sshKey }) => {
|
|
1855
|
+
const options = await apiRequest("GET", svc(orderNo, "change-os"));
|
|
1856
|
+
const capability = options.data;
|
|
1857
|
+
if (options.status >= 400 || capability?.success !== true) {
|
|
1858
|
+
return { isError: true, content: [{ type: "text", text: formatJson(options.data) }] };
|
|
1859
|
+
}
|
|
1743
1860
|
const body = { osVersion };
|
|
1861
|
+
if (capability.dedicated === true) {
|
|
1862
|
+
if (confirmReinstall !== true)
|
|
1863
|
+
throw new Error("Dedicated reinstall requires explicit user approval of all-disk data loss and confirmReinstall=true.");
|
|
1864
|
+
if (rootPassword !== undefined)
|
|
1865
|
+
throw new Error("Dedicated reinstall does not support rootPassword; use an account sshKey or reuse the installed key.");
|
|
1866
|
+
body.confirmReinstall = true;
|
|
1867
|
+
if (sshKey !== undefined)
|
|
1868
|
+
body.sshKey = sshKey;
|
|
1869
|
+
}
|
|
1870
|
+
else if (confirmReinstall !== undefined || sshKey !== undefined) {
|
|
1871
|
+
throw new Error("confirmReinstall and sshKey are dedicated-only reinstall fields; inspect get_os_options for this service.");
|
|
1872
|
+
}
|
|
1744
1873
|
if (rootPassword)
|
|
1745
1874
|
body.rootPassword = rootPassword;
|
|
1746
1875
|
if (confirmSnapshotDelete === true)
|
|
@@ -1846,6 +1975,22 @@ export function createVpsnetServer(options = {}) {
|
|
|
1846
1975
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1847
1976
|
});
|
|
1848
1977
|
// --- Order New VPS ---
|
|
1978
|
+
// Keep only a hash of configuration (which may contain a root password), plus
|
|
1979
|
+
// the non-secret quote. The backend refuses a second quote for the same key.
|
|
1980
|
+
const serviceOrderQuoteCapacity = 1000;
|
|
1981
|
+
const serviceOrderQuotesInFlight = new Set();
|
|
1982
|
+
const serviceOrderQuotes = new Map();
|
|
1983
|
+
const orderMoneySchema = z.union([z.number().finite().nonnegative(), z.string()])
|
|
1984
|
+
.refine(value => /^(?:0|[1-9][0-9]{0,8})(?:\.[0-9]{1,4})?$/.test(String(value)), "Use an exact nonnegative EUR amount with at most four decimal places");
|
|
1985
|
+
const orderMoneyUnits = (value) => {
|
|
1986
|
+
const [whole, fraction = ""] = String(value).split(".");
|
|
1987
|
+
return BigInt(whole) * 10000n + BigInt(fraction.padEnd(4, "0"));
|
|
1988
|
+
};
|
|
1989
|
+
const orderConfigurationHash = (body) => {
|
|
1990
|
+
const canonical = (value) => Array.isArray(value) ? value.map(canonical)
|
|
1991
|
+
: value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, canonical(entry)])) : value;
|
|
1992
|
+
return createHash("sha256").update(JSON.stringify(canonical(body))).digest("hex");
|
|
1993
|
+
};
|
|
1849
1994
|
server.registerTool("get_order_plans", {
|
|
1850
1995
|
description: "Get available plans for one explicitly selected service product. Product facts are unordered: 'firecracker' is VPS using Firecracker microVMs for Linux; 'vds' is Cloud VPS (KVM) with High Availability, replicated Ceph NVMe storage, and Linux/Windows/BSD support; 'vps' is Container VPS for Linux; 'ds' is a dedicated single-tenant server. Select from the user's requirements and constraints rather than list position. Firecracker Functions is a separate usage-billed product and is not an orderable service type here.",
|
|
1851
1996
|
inputSchema: {
|
|
@@ -1870,7 +2015,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1870
2015
|
description: [
|
|
1871
2016
|
"Order a new VPS. Requires sufficient account balance for balance payment.",
|
|
1872
2017
|
"Payment object for balance: { payment: 1, successUrl: '', cancelUrl: '' }.",
|
|
1873
|
-
"
|
|
2018
|
+
"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.",
|
|
1874
2019
|
"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.",
|
|
1875
2020
|
"Resources: array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
|
|
1876
2021
|
"rootPassword: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. Example: 'MyPass123'.",
|
|
@@ -1888,7 +2033,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1888
2033
|
.number()
|
|
1889
2034
|
.optional()
|
|
1890
2035
|
.describe("SSH key ID from list_ssh_keys to deploy. Mutually exclusive with rootPassword"),
|
|
1891
|
-
period: positiveIdSchema.
|
|
2036
|
+
period: positiveIdSchema.describe("Explicit billing period ID from get_order_options; disclose its term together with the exact gross quote before approval."),
|
|
1892
2037
|
resources: z
|
|
1893
2038
|
.array(z.number())
|
|
1894
2039
|
.optional()
|
|
@@ -1896,7 +2041,10 @@ export function createVpsnetServer(options = {}) {
|
|
|
1896
2041
|
privateNetworkId: privateNetworkIdSchema
|
|
1897
2042
|
.optional()
|
|
1898
2043
|
.describe("UUID of one of your private networks to attach the new server to (shown in the control panel under Networking). Sent to both the quote and the confirm; omit for no private network"),
|
|
1899
|
-
idempotencyKey:
|
|
2044
|
+
idempotencyKey: paidIdempotencyKeySchema.describe("Required for paid API-key orders. Stable unique key for this exact order attempt, e.g. UUID."),
|
|
2045
|
+
quoteToken: z.string().min(32).max(190).optional().describe("Exact token returned by the first order_service call; required for confirmation."),
|
|
2046
|
+
expected_total_eur: orderMoneySchema.optional().describe("Exact VAT-inclusive amount_eur disclosed to and approved by the customer; required for confirmation."),
|
|
2047
|
+
acknowledge_charge: z.literal(true).optional().describe("Set only after explicit approval of the exact gross EUR total and billing period."),
|
|
1900
2048
|
payment: z
|
|
1901
2049
|
.object({
|
|
1902
2050
|
payment: z
|
|
@@ -1912,7 +2060,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1912
2060
|
.passthrough()
|
|
1913
2061
|
.describe("Payment object. For balance: { payment: 1, successUrl: '', cancelUrl: '' }"),
|
|
1914
2062
|
},
|
|
1915
|
-
}, async ({ plan, os, rootPassword, sshKey, period, resources, privateNetworkId, idempotencyKey, payment }) => {
|
|
2063
|
+
}, async ({ plan, os, rootPassword, sshKey, period, resources, privateNetworkId, idempotencyKey, payment, quoteToken: approvedToken, expected_total_eur, acknowledge_charge }) => {
|
|
1916
2064
|
const body = { plan, payment, idempotencyKey };
|
|
1917
2065
|
if (privateNetworkId !== undefined)
|
|
1918
2066
|
body.privateNetworkId = privateNetworkId;
|
|
@@ -1926,17 +2074,70 @@ export function createVpsnetServer(options = {}) {
|
|
|
1926
2074
|
body.period = period;
|
|
1927
2075
|
if (resources)
|
|
1928
2076
|
body.resources = resources;
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
2077
|
+
const hash = orderConfigurationHash(body);
|
|
2078
|
+
const prior = serviceOrderQuotes.get(idempotencyKey);
|
|
2079
|
+
const refusal = (error, message) => ({ isError: true,
|
|
2080
|
+
content: [{ type: "text", text: formatJson({ success: false, error, confirmationRequired: true, idempotencyKey, message }) }] });
|
|
2081
|
+
if (prior && prior.configurationHash !== hash)
|
|
2082
|
+
return refusal("orderConfigurationChanged", "Reuse this key only with the unchanged quoted order configuration. No confirmation was sent.");
|
|
2083
|
+
const confirming = approvedToken !== undefined || expected_total_eur !== undefined || acknowledge_charge !== undefined;
|
|
2084
|
+
if (confirming) {
|
|
2085
|
+
if (!prior)
|
|
2086
|
+
return refusal("orderQuoteUnavailable", "This process has no original quote for this key. Inspect the original attempt in the VPSnet panel; do not create a new key to retry an uncertain purchase.");
|
|
2087
|
+
if (acknowledge_charge !== true || expected_total_eur === undefined || approvedToken !== prior.quoteToken) {
|
|
2088
|
+
return refusal("purchaseConfirmationRequired", "Approve the exact quoted gross EUR amount and billing period, then supply all confirmation fields and the original quote token. No confirmation was sent.");
|
|
2089
|
+
}
|
|
2090
|
+
if (orderMoneyUnits(expected_total_eur) !== prior.total)
|
|
2091
|
+
return refusal("orderQuoteChanged", "The approved gross EUR total differs from the original quote. Show that quote and obtain approval again. No confirmation was sent.");
|
|
2092
|
+
// A concurrent call, exact retry or uncertain transport result must not
|
|
2093
|
+
// send another confirmation. Retain the original outcome for this key.
|
|
2094
|
+
prior.confirmation ??= (async () => {
|
|
2095
|
+
try {
|
|
2096
|
+
const { status, data } = await apiRequest("POST", "/order/configuration/confirm", { ...body, quoteToken: prior.quoteToken }, { "Idempotency-Key": idempotencyKey, "X-Quote-Token": prior.quoteToken });
|
|
2097
|
+
const response = data;
|
|
2098
|
+
if (response?.quoteMismatch === true || response?.quotePayloadMismatch === true || response?.quoteExpired === true) {
|
|
2099
|
+
return { isError: true, content: [{ type: "text", text: formatJson({ ...response,
|
|
2100
|
+
confirmationRequired: true, idempotencyKey,
|
|
2101
|
+
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." }) }] };
|
|
2102
|
+
}
|
|
2103
|
+
return { ...(status >= 400 ? { isError: true } : {}), content: [{ type: "text", text: formatJson(data) }] };
|
|
2104
|
+
}
|
|
2105
|
+
catch {
|
|
2106
|
+
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.");
|
|
2107
|
+
}
|
|
2108
|
+
})();
|
|
2109
|
+
return prior.confirmation;
|
|
1932
2110
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2111
|
+
if (prior)
|
|
2112
|
+
return prior.confirmation ?? { content: [{ type: "text", text: formatJson(prior.quote) }] };
|
|
2113
|
+
if (serviceOrderQuotesInFlight.has(idempotencyKey)) {
|
|
2114
|
+
return refusal("orderQuotePending", "The original quote request is still pending. Wait for that result and keep the same key; no second quote or confirmation was sent.");
|
|
2115
|
+
}
|
|
2116
|
+
if (serviceOrderQuotes.size + serviceOrderQuotesInFlight.size >= serviceOrderQuoteCapacity) {
|
|
2117
|
+
return refusal("orderAttemptCapacityReached", "This MCP process has reached its retained order-attempt capacity. No new quote or confirmation was sent. Existing attempts remain available under their original keys. Inspect those attempts in the VPSnet panel; do not restart or replace a key to retry an uncertain purchase.");
|
|
2118
|
+
}
|
|
2119
|
+
serviceOrderQuotesInFlight.add(idempotencyKey);
|
|
2120
|
+
try {
|
|
2121
|
+
const { status: quoteStatus, data: quoteData } = await apiRequest("POST", "/order/configuration/quote", body, { "Idempotency-Key": idempotencyKey });
|
|
2122
|
+
if (quoteStatus >= 400 || quoteData?.success === false) {
|
|
2123
|
+
return { content: [{ type: "text", text: formatJson(quoteData) }], isError: true };
|
|
2124
|
+
}
|
|
2125
|
+
const source = quoteData;
|
|
2126
|
+
const token = z.string().min(32).max(190).safeParse(source?.quoteToken);
|
|
2127
|
+
const amount = orderMoneySchema.safeParse(source?.amount_eur);
|
|
2128
|
+
if (!token.success || !amount.success)
|
|
2129
|
+
return refusal("orderQuoteInvalid", "The API did not return a valid exact gross EUR quote and token. No confirmation was sent. Inspect this original attempt in the VPSnet panel.");
|
|
2130
|
+
const quote = { success: true, confirmationRequired: true, idempotencyKey,
|
|
2131
|
+
quoteToken: token.data, amount_eur: amount.data, currency: "EUR", amount_includes_vat: true,
|
|
2132
|
+
plan, period,
|
|
2133
|
+
...(typeof source?.quoteExpiresAt === "string" || typeof source?.quoteExpiresAt === "number" ? { quoteExpiresAt: source.quoteExpiresAt } : {}),
|
|
2134
|
+
message: "No purchase was confirmed. Show this gross EUR total and billing period, obtain explicit approval, then call order_service with the unchanged configuration and key plus quoteToken, expected_total_eur and acknowledge_charge=true." };
|
|
2135
|
+
serviceOrderQuotes.set(idempotencyKey, { configurationHash: hash, total: orderMoneyUnits(amount.data), quoteToken: token.data, quote });
|
|
2136
|
+
return { content: [{ type: "text", text: formatJson(quote) }] };
|
|
2137
|
+
}
|
|
2138
|
+
finally {
|
|
2139
|
+
serviceOrderQuotesInFlight.delete(idempotencyKey);
|
|
1936
2140
|
}
|
|
1937
|
-
body.quoteToken = quoteToken;
|
|
1938
|
-
const { data } = await apiRequest("POST", "/order/configuration/confirm", body, { "Idempotency-Key": idempotencyKey, "X-Quote-Token": quoteToken });
|
|
1939
|
-
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1940
2141
|
});
|
|
1941
2142
|
// --- Backups ---
|
|
1942
2143
|
server.registerTool("get_backup_status", {
|
|
@@ -1952,15 +2153,17 @@ export function createVpsnetServer(options = {}) {
|
|
|
1952
2153
|
description: "Get backup history for a service Requires services:read when called with an API key.",
|
|
1953
2154
|
inputSchema: {
|
|
1954
2155
|
orderNo: serviceOrderNoSchema,
|
|
2156
|
+
page: z.number().int().min(1).max(1000000).optional().describe("Page number; follow response pagination for older records."),
|
|
1955
2157
|
},
|
|
1956
|
-
}, async ({ orderNo }) => {
|
|
1957
|
-
const { data } = await apiRequest("GET", svc(orderNo, "backup/history"));
|
|
2158
|
+
}, async ({ orderNo, page }) => {
|
|
2159
|
+
const { data } = await apiRequest("GET", svc(orderNo, "backup/history") + (page === undefined ? "" : `?page=${page}`));
|
|
1958
2160
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1959
2161
|
});
|
|
1960
2162
|
server.registerTool("create_backup", {
|
|
1961
|
-
description: "Create a new backup. Returns noty UUID for tracking. First call get_backup_status to see available period dates and price. Backup is a paid operation (price shown in get_backup_status). Requires services:manage and a full-access API key.",
|
|
2163
|
+
description: "Create a new backup. Returns noty UUID for tracking. First call get_backup_status to see available period dates and price. Backup is a paid operation (price shown in get_backup_status). Call only after the user approves the exact service, historical date, directories and disclosed price. Reuse the same idempotencyKey and unchanged request after a lost response; never rotate the key to retry an uncertain payment. Requires services:manage and a full-access API key.",
|
|
1962
2164
|
inputSchema: {
|
|
1963
2165
|
orderNo: serviceOrderNoSchema,
|
|
2166
|
+
idempotencyKey: paidIdempotencyKeySchema.describe("Stable unique key for this exact approved backup purchase: 16-190 ASCII letters, digits, dots, underscores, colons or hyphens; first character must be alphanumeric. Reuse unchanged on retry."),
|
|
1964
2167
|
period: z
|
|
1965
2168
|
.string()
|
|
1966
2169
|
.describe("Backup date in YYYY-MM-DD format. Must be one of the dates from get_backup_status options (up to 7 days in the past)"),
|
|
@@ -1982,20 +2185,46 @@ export function createVpsnetServer(options = {}) {
|
|
|
1982
2185
|
.passthrough()
|
|
1983
2186
|
.describe("Payment object. For balance: { payment: 1, successUrl: '', cancelUrl: '' }"),
|
|
1984
2187
|
},
|
|
1985
|
-
}, async ({ orderNo, period, directories, payment }) => {
|
|
2188
|
+
}, async ({ orderNo, period, directories, payment, idempotencyKey }) => {
|
|
1986
2189
|
const { data } = await apiRequest("POST", svc(orderNo, "backup"), {
|
|
1987
2190
|
period,
|
|
1988
2191
|
directories,
|
|
1989
2192
|
payment,
|
|
1990
|
-
});
|
|
2193
|
+
}, { "Idempotency-Key": idempotencyKey });
|
|
2194
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2195
|
+
});
|
|
2196
|
+
// Firecracker nightly backups have their own customer read model; the legacy
|
|
2197
|
+
// /backup endpoints describe Container VPS archive purchases instead.
|
|
2198
|
+
server.registerTool("get_firecracker_backup_status", {
|
|
2199
|
+
description: "Get nightly off-node backup status for a Firecracker VPS, including running count and the latest retained backup's consistency provenance. A guest_agent attestation is the guest's claim. Temporary VMs have no automatic backups. Requires services:read.",
|
|
2200
|
+
inputSchema: { orderNo: serviceOrderNoSchema },
|
|
2201
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
2202
|
+
}, async ({ orderNo }) => {
|
|
2203
|
+
const { data } = await apiRequest("GET", svc(orderNo, "firecracker/backup/status"));
|
|
2204
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2205
|
+
});
|
|
2206
|
+
server.registerTool("get_firecracker_backup_history", {
|
|
2207
|
+
description: "Get retained nightly Firecracker VPS backup history, including failed/running attempts and consistency provenance. records and backups contain the same list; this endpoint is not paginated. Temporary VMs return an empty disabled history. Requires services:read.",
|
|
2208
|
+
inputSchema: { orderNo: serviceOrderNoSchema },
|
|
2209
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
2210
|
+
}, async ({ orderNo }) => {
|
|
2211
|
+
const { data } = await apiRequest("GET", svc(orderNo, "firecracker/backup/history"));
|
|
1991
2212
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1992
2213
|
});
|
|
1993
2214
|
// --- SSH Keys ---
|
|
1994
2215
|
server.registerTool("list_ssh_keys", {
|
|
1995
|
-
description: "List
|
|
1996
|
-
inputSchema: {
|
|
1997
|
-
|
|
1998
|
-
|
|
2216
|
+
description: "List account SSH public keys. Follow response pagination until all pages are read; perPage accepts 1-200 (default 50).",
|
|
2217
|
+
inputSchema: {
|
|
2218
|
+
page: z.number().int().min(1).max(1000000).optional(),
|
|
2219
|
+
perPage: z.number().int().min(1).max(200).optional(),
|
|
2220
|
+
},
|
|
2221
|
+
}, async ({ page, perPage }) => {
|
|
2222
|
+
const query = new URLSearchParams();
|
|
2223
|
+
if (page !== undefined)
|
|
2224
|
+
query.set("page", String(page));
|
|
2225
|
+
if (perPage !== undefined)
|
|
2226
|
+
query.set("perPage", String(perPage));
|
|
2227
|
+
const { data } = await apiRequest("GET", "/account/ssh-keys" + (query.size ? `?${query}` : ""));
|
|
1999
2228
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2000
2229
|
});
|
|
2001
2230
|
server.registerTool("get_ssh_key", {
|
|
@@ -2988,15 +3217,10 @@ export function createVpsnetServer(options = {}) {
|
|
|
2988
3217
|
inputSchema: {},
|
|
2989
3218
|
}, getDomainOrderingStatus);
|
|
2990
3219
|
server.registerTool("list_domain_contacts", {
|
|
2991
|
-
description: "List domain registrant/admin/tech/billing contacts owned by the account. Requires domains:read when using an API key.",
|
|
2992
|
-
inputSchema: {
|
|
2993
|
-
}, async (
|
|
2994
|
-
const
|
|
2995
|
-
if (page !== undefined)
|
|
2996
|
-
query.set('page', String(page));
|
|
2997
|
-
if (snapshot !== undefined)
|
|
2998
|
-
query.set('snapshot', snapshot);
|
|
2999
|
-
const { data } = await apiRequest("GET", "/account/domains/contacts" + (query.size ? "?" + query.toString() : ""));
|
|
3220
|
+
description: "List all domain registrant/admin/tech/billing contacts owned by the account in one response. This endpoint has no pagination or snapshot parameters. Requires domains:read when using an API key.",
|
|
3221
|
+
inputSchema: {},
|
|
3222
|
+
}, async () => {
|
|
3223
|
+
const { data } = await apiRequest("GET", "/account/domains/contacts");
|
|
3000
3224
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3001
3225
|
});
|
|
3002
3226
|
const domainContactInputSchema = {
|
|
@@ -3626,6 +3850,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3626
3850
|
for (const key of [
|
|
3627
3851
|
"name",
|
|
3628
3852
|
"runtime_os_id",
|
|
3853
|
+
"private_network_id",
|
|
3629
3854
|
"entrypoint",
|
|
3630
3855
|
"description",
|
|
3631
3856
|
"code",
|
|
@@ -3646,6 +3871,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3646
3871
|
const functionFieldsSchema = {
|
|
3647
3872
|
name: z.string().optional().describe("Function name"),
|
|
3648
3873
|
runtime_os_id: positiveIdSchema.optional().describe("Runtime OS ID (see list_functions response for available runtimes)"),
|
|
3874
|
+
private_network_id: z.string().regex(/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/).nullable().optional().describe("Private network ID from list_functions privateNetworking discovery. Omit to preserve an existing binding during update; null detaches it. Backend validates ownership and availability."),
|
|
3649
3875
|
entrypoint: z.string().optional().describe("Entrypoint command/handler"),
|
|
3650
3876
|
description: z.string().optional().describe("Description (max 2000 chars)"),
|
|
3651
3877
|
code: z.string().optional().describe("Function source code"),
|
|
@@ -3664,6 +3890,25 @@ export function createVpsnetServer(options = {}) {
|
|
|
3664
3890
|
const { data } = await apiRequest("GET", "/account/firecracker/functions");
|
|
3665
3891
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3666
3892
|
});
|
|
3893
|
+
server.registerTool("list_function_history", {
|
|
3894
|
+
description: "Read paginated account-wide Firecracker Function invocation history. Filter by function, status or search and follow pagination.pages for older runs. Requires services:read for an API key; OAuth uses the Functions read permission.",
|
|
3895
|
+
inputSchema: {
|
|
3896
|
+
function_id: positiveIdSchema.optional().describe("Optional Function ID from list_functions"),
|
|
3897
|
+
status: z.enum(["queued", "running", "succeeded", "failed", "timed_out", "error"]).optional(),
|
|
3898
|
+
search: z.string().max(200).optional(),
|
|
3899
|
+
limit: z.number().int().min(1).max(200).optional().describe("Page size; default 25"),
|
|
3900
|
+
page: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).optional().describe("Page number; default 1"),
|
|
3901
|
+
},
|
|
3902
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
3903
|
+
}, async ({ function_id, status, search, limit, page }) => {
|
|
3904
|
+
const query = new URLSearchParams();
|
|
3905
|
+
for (const [key, value] of Object.entries({ function_id, status, search, limit, page })) {
|
|
3906
|
+
if (value !== undefined)
|
|
3907
|
+
query.set(key, String(value));
|
|
3908
|
+
}
|
|
3909
|
+
const { data } = await apiRequest("GET", `/account/firecracker/functions/history${query.size ? `?${query}` : ""}`);
|
|
3910
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3911
|
+
});
|
|
3667
3912
|
server.registerTool("get_function", {
|
|
3668
3913
|
description: "Get one Firecracker Function with code, config, and webhook details. OAuth reads redact environment values and webhook secrets; omit those fields during unrelated edits.",
|
|
3669
3914
|
inputSchema: {
|
|
@@ -3744,7 +3989,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3744
3989
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3745
3990
|
});
|
|
3746
3991
|
server.registerTool("list_function_invocations", {
|
|
3747
|
-
description: "List invocations of a Firecracker Function with status, duration, and cost.",
|
|
3992
|
+
description: "List the latest 50 invocations of a Firecracker Function with status, duration, and cost. Use list_function_history for older runs and pagination.",
|
|
3748
3993
|
inputSchema: {
|
|
3749
3994
|
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3750
3995
|
},
|
|
@@ -3948,7 +4193,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3948
4193
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3949
4194
|
});
|
|
3950
4195
|
server.registerTool("create_dns_zone", {
|
|
3951
|
-
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 TXT
|
|
4196
|
+
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.",
|
|
3952
4197
|
inputSchema: {
|
|
3953
4198
|
zone: z.string().describe("Zone name, e.g. example.com"),
|
|
3954
4199
|
kind: dnsZoneKindSchema
|
|
@@ -3994,7 +4239,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3994
4239
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3995
4240
|
});
|
|
3996
4241
|
server.registerTool("get_dns_zone", {
|
|
3997
|
-
description: "Get a DNS zone and desired records. Requires dns:read when using an API key.",
|
|
4242
|
+
description: "Get a DNS zone and desired records, including their record IDs for update_dns_record. A waiting zone's ownership TXT value is available in zone.verification.record.value when recoverable; read it before considering reissue_dns_zone_verification. Requires dns:read when using an API key.",
|
|
3998
4243
|
inputSchema: {
|
|
3999
4244
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4000
4245
|
},
|
|
@@ -4089,6 +4334,16 @@ export function createVpsnetServer(options = {}) {
|
|
|
4089
4334
|
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}`);
|
|
4090
4335
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4091
4336
|
});
|
|
4337
|
+
server.registerTool("reapply_dns_zone", {
|
|
4338
|
+
description: "Retry publication of a published DNS zone whose apply_state is failed. Read get_dns_zone first: fix any apply_blocking_records before retrying. This queues the existing desired records for publication and can change public DNS answers; success with apply_state=queued does not mean publication completed. Poll get_dns_zone until apply_state is clean or failed, inspecting apply_error on failure. The API refuses zones that are not published, not failed, or still blocked by invalid records. Requires dns:write when using an API key.",
|
|
4339
|
+
inputSchema: {
|
|
4340
|
+
zone_id: positiveIdSchema.describe("DNS zone ID from get_dns_zone with status=published and apply_state=failed"),
|
|
4341
|
+
},
|
|
4342
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
4343
|
+
}, async ({ zone_id }) => {
|
|
4344
|
+
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/reapply`);
|
|
4345
|
+
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4346
|
+
});
|
|
4092
4347
|
server.registerTool("verify_dns_zone", {
|
|
4093
4348
|
description: "Check the zone's ownership TXT value (_vpsnet-dns.<zone>) and publish the zone when it matches: publicly, or served authoritatively by the nameservers the parent zone delegated the name to when the zone was created (the previous DNS provider) - so a domain already pointed at ns1/ns2.vpsnet.com is verified by adding the TXT at the previous provider, never by moving the nameservers back. Delegation to VPSnet nameservers alone is never proof. A refusal (verificationMissing) returns verification.checks (every place checked and what was found) and verification.next_step: add_txt | add_txt_at_previous_provider | add_txt_in_parent_zone | contact_support (only VPSnet support can confirm the domain; the zone is kept, not deleted, while the domain points at VPSnet). lookupUnavailable (HTTP 503) means the platform could not run the DNS check - retry later; it is not a missing record. Requires dns:write when using an API key.",
|
|
4094
4349
|
inputSchema: {
|
|
@@ -4100,7 +4355,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4100
4355
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4101
4356
|
});
|
|
4102
4357
|
server.registerTool("reissue_dns_zone_verification", {
|
|
4103
|
-
description: "
|
|
4358
|
+
description: "Rotate the ownership TXT value for a DNS zone in pending_verification or verification_expired. First read get_dns_zone.zone.verification.record.value; reissue only when that value is null/unrecoverable or the customer requests rotation. Returns the new value in verificationRecord; the previous value stops being accepted, so update the TXT at the location indicated by zone.verification.next_step, then call verify_dns_zone. Reissuing does not itself verify the zone or reset its status. Rate limited to 5 per minute. Requires dns:write when using an API key.",
|
|
4104
4359
|
inputSchema: {
|
|
4105
4360
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4106
4361
|
},
|
|
@@ -4136,7 +4391,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4136
4391
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4137
4392
|
});
|
|
4138
4393
|
server.registerTool("upsert_dns_record", {
|
|
4139
|
-
description: "
|
|
4394
|
+
description: "Add a forward DNS desired-state record to a native zone, identified by name, type and content together. Different content adds another record and keeps existing values; it does not replace them. An identical tuple updates its TTL/comment and re-enables it if disabled. To replace an existing record's content, read get_dns_zone and pass its exact record_id to update_dns_record. Changes affect public answers after publication. PTR and *.arpa are rejected; use change_rdns for reverse DNS. Requires dns:write when using an API key.",
|
|
4140
4395
|
inputSchema: {
|
|
4141
4396
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4142
4397
|
name: z
|
|
@@ -4315,17 +4570,29 @@ export function createVpsnetServer(options = {}) {
|
|
|
4315
4570
|
});
|
|
4316
4571
|
// --- History ---
|
|
4317
4572
|
server.registerTool("get_login_history", {
|
|
4318
|
-
description: "Get account login history (IPs, dates)",
|
|
4319
|
-
inputSchema: {},
|
|
4320
|
-
}, async () => {
|
|
4321
|
-
const { data } = await apiRequest("GET", "/account/history/login");
|
|
4573
|
+
description: "Get account login history (IPs, dates) with pagination; follow response pagination to read older records.",
|
|
4574
|
+
inputSchema: { page: z.number().int().min(1).max(1000000).optional() },
|
|
4575
|
+
}, async ({ page }) => {
|
|
4576
|
+
const { data } = await apiRequest("GET", "/account/history/login" + (page === undefined ? "" : `?page=${page}`));
|
|
4322
4577
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4323
4578
|
});
|
|
4324
4579
|
server.registerTool("get_management_history", {
|
|
4325
|
-
description: "Get management/activity history (
|
|
4326
|
-
inputSchema: {
|
|
4327
|
-
|
|
4328
|
-
|
|
4580
|
+
description: "Get management/activity history with pagination and optional exact orderNo or subject_kind/subject_key filters. subject_kind accepts one kind or comma-separated kinds (network, load_balancer, vpn_gateway, nat_gateway, security_group, public_address); subject_key requires exactly one kind. Follow response pagination to read older records.",
|
|
4581
|
+
inputSchema: {
|
|
4582
|
+
page: z.number().int().min(1).max(1000000).optional(),
|
|
4583
|
+
orderNo: serviceOrderNoSchema.optional(),
|
|
4584
|
+
subject_kind: z.string().max(255).regex(/^(?:network|load_balancer|vpn_gateway|nat_gateway|security_group|public_address)(?:,(?:network|load_balancer|vpn_gateway|nat_gateway|security_group|public_address))*$/).optional(),
|
|
4585
|
+
subject_key: z.string().trim().min(1).max(255).optional(),
|
|
4586
|
+
},
|
|
4587
|
+
}, async ({ page, orderNo, subject_kind, subject_key }) => {
|
|
4588
|
+
if (subject_key !== undefined && (subject_kind === undefined || subject_kind.includes(","))) {
|
|
4589
|
+
return { isError: true, content: [{ type: "text", text: formatJson({ success: false, error: "managementHistorySubjectFilterInvalid", message: "subject_key requires exactly one subject_kind." }) }] };
|
|
4590
|
+
}
|
|
4591
|
+
const query = new URLSearchParams();
|
|
4592
|
+
for (const [key, value] of Object.entries({ page, orderNo, subject_kind, subject_key }))
|
|
4593
|
+
if (value !== undefined)
|
|
4594
|
+
query.set(key, String(value));
|
|
4595
|
+
const { data } = await apiRequest("GET", "/account/history/management" + (query.size ? "?" + query.toString() : ""));
|
|
4329
4596
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4330
4597
|
});
|
|
4331
4598
|
server.registerTool("get_usage_statements", {
|