vpsnet-mcp 2.3.5 → 2.3.7
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 +28 -8
- package/build/certificate-contract.js +2 -2
- package/build/index.js +266 -113
- package/build/networking-tools.js +5 -5
- package/build/remote-policy.js +38 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ replacement is conclusively contained; uncertain outcomes fail closed.
|
|
|
153
153
|
|
|
154
154
|
An immutable update is available only when `get_application_installation`
|
|
155
155
|
returns an `available_actions` entry with `type: "update"`. After explicit user
|
|
156
|
-
confirmation, call `
|
|
156
|
+
confirmation, call `update_application` with the exact
|
|
157
157
|
advertised `expected_blueprint_version` and `expected_upstream_version`, and a
|
|
158
158
|
new idempotency key. Keys are client-global: reuse a key only to replay the
|
|
159
159
|
exact same request, never for another service or operation. The
|
|
@@ -186,7 +186,7 @@ rather than attempting an action and reading the failure.
|
|
|
186
186
|
|
|
187
187
|
Uninstall permanently deletes the managed containers, configuration, saved
|
|
188
188
|
credentials, and application data; existing server backups are retained. The
|
|
189
|
-
`
|
|
189
|
+
`uninstall_application` call requires `acknowledge_data_loss=true`,
|
|
190
190
|
and it must only be set after explicit user confirmation.
|
|
191
191
|
|
|
192
192
|
The MCP surface is task-oriented rather than a one-to-one mirror of every REST
|
|
@@ -224,7 +224,8 @@ paying twice for names already covered by base/www or wildcard/base rules.
|
|
|
224
224
|
During the final 30 days, an eligible next term uses
|
|
225
225
|
`quote_automatic_ssl_renewal` → `order_automatic_ssl_renewal` and is prepaid
|
|
226
226
|
from account balance only after the exact EUR total is approved. Cancellation,
|
|
227
|
-
domain removal, and same-type correction use `
|
|
227
|
+
domain removal, and same-type correction use `cancel_automatic_ssl_subscription`,
|
|
228
|
+
`remove_automatic_ssl_domain`, and `replace_automatic_ssl_domain`;
|
|
228
229
|
ambiguous state is reconciled with `refresh_automatic_ssl_subscription`, never
|
|
229
230
|
by inventing a second mutation. Private ACME server and EAB credentials stay
|
|
230
231
|
in the two-factor-protected customer portal and are intentionally unavailable
|
|
@@ -241,7 +242,7 @@ There is no dialog on this surface to collect that confirmation, so the tool
|
|
|
241
242
|
does not manufacture it. `acknowledge_data_replacement` and
|
|
242
243
|
`acknowledge_restore_charge` must each be `true`, and are only to be set after
|
|
243
244
|
the user has actually approved the disk replacement and the charge — the same
|
|
244
|
-
rule as `acknowledge_data_loss` on `
|
|
245
|
+
rule as `acknowledge_data_loss` on `uninstall_application`. Omitting either is a
|
|
245
246
|
schema error, so an unattended caller cannot fall through to a charge: nothing
|
|
246
247
|
is even quoted.
|
|
247
248
|
|
|
@@ -644,7 +645,14 @@ everything else.
|
|
|
644
645
|
| `install_application` | Queue a confirmed, version-pinned managed installation | `applications:manage` | free |
|
|
645
646
|
| `configure_application_access` | Queue a confirmed platform-hostname, private, public-IP, managed-HTTPS, or customer-managed external-HTTPS access change | `applications:manage` | free |
|
|
646
647
|
| `configure_application_resource_thresholds` | Replace confirmed non-enforcing application resource thresholds and reached/recovered email preference | `applications:manage` | free |
|
|
647
|
-
| `manage_application` |
|
|
648
|
+
| `manage_application` | Compatibility tool for older clients; use the individual actions below | `applications:manage` | free |
|
|
649
|
+
| `reconcile_application` | Reconcile an application with its declared configuration | `applications:manage` | free |
|
|
650
|
+
| `repair_application` | Repair an application runtime | `applications:manage` | free |
|
|
651
|
+
| `restart_application` | Restart an application after confirming interruption | `applications:manage` | free |
|
|
652
|
+
| `start_application` | Start a stopped application | `applications:manage` | free |
|
|
653
|
+
| `stop_application` | Stop an application after confirming interruption | `applications:manage` | free |
|
|
654
|
+
| `uninstall_application` | Delete an application after explicit data-loss approval | `applications:manage` | free |
|
|
655
|
+
| `update_application` | Apply the eligible immutable update with exact version preconditions | `applications:manage` | free |
|
|
648
656
|
| `cancel_application_action` | Cancel the exact latest queued action only while the backend advertises it as cancellable | `applications:manage` | free |
|
|
649
657
|
|
|
650
658
|
### Service Actions
|
|
@@ -752,7 +760,11 @@ cannot be performed by an MCP connection authenticated with an API key.
|
|
|
752
760
|
| `get_free_certificate_instruction` | Read automatic-DNS status or every required external-DNS CNAME | `certificates:read` | free |
|
|
753
761
|
| `create_free_certificate` | Create one explicitly approved, idempotent no-cost DV request | `certificates:manage` | free |
|
|
754
762
|
| `download_free_certificate` | Retrieve only the issued public leaf and chain, never a private key | `certificates:read` | free |
|
|
755
|
-
| `manage_free_certificate` |
|
|
763
|
+
| `manage_free_certificate` | Compatibility tool for older clients; use the individual actions below | `certificates:manage` | free |
|
|
764
|
+
| `renew_free_certificate` | Request early renewal for the authorized certificate names | `certificates:manage` | free |
|
|
765
|
+
| `revoke_free_certificate` | Revoke an owned certificate after explicit approval | `certificates:manage` | free |
|
|
766
|
+
| `recheck_free_certificate` | Recheck validation for a pending certificate | `certificates:manage` | free |
|
|
767
|
+
| `cancel_free_certificate` | Cancel an eligible pending certificate request | `certificates:manage` | free |
|
|
756
768
|
|
|
757
769
|
For portable API or assistant installation, use `customer_csr` and retain the
|
|
758
770
|
private key where the CSR was generated. A managed request can renew unattended,
|
|
@@ -773,7 +785,10 @@ not treat a queued request as issued.
|
|
|
773
785
|
| `order_automatic_ssl_domain` | Confirm and pay for the explicitly approved DNS-name quote | `certificates:read` | Paid: `certificates:order` |
|
|
774
786
|
| `quote_automatic_ssl_renewal` | Quote the next eligible subscription term in EUR | `certificates:read` | Quote: `certificates:order` |
|
|
775
787
|
| `order_automatic_ssl_renewal` | Prepay the explicitly approved next term from account balance | `certificates:read` | Paid: `certificates:order` |
|
|
776
|
-
| `manage_automatic_ssl_subscription` |
|
|
788
|
+
| `manage_automatic_ssl_subscription` | Compatibility tool for older clients; use the individual actions below | `certificates:manage` | free |
|
|
789
|
+
| `cancel_automatic_ssl_subscription` | Cancel an owned Automatic SSL subscription | `certificates:manage` | free |
|
|
790
|
+
| `remove_automatic_ssl_domain` | Remove one eligible DNS name from a subscription | `certificates:manage` | free |
|
|
791
|
+
| `replace_automatic_ssl_domain` | Correct an eligible DNS name within its correction window | `certificates:manage` | free |
|
|
777
792
|
|
|
778
793
|
Automatic SSL is a portable paid ACME subscription. It works with a compatible
|
|
779
794
|
client on VPSnet or another provider; it is not limited to managed
|
|
@@ -794,7 +809,12 @@ tool or API-key response, so an assistant cannot copy them into model context.
|
|
|
794
809
|
| `download_certificate` | Download the issued public leaf and chain; private keys are never returned | `certificates:read` | free |
|
|
795
810
|
| `list_certificate_actions` | List durable customer-visible management actions | `certificates:read` | free |
|
|
796
811
|
| `refresh_certificate` | Schedule a read-only certificate-authority status reconciliation | `certificates:manage` | free |
|
|
797
|
-
| `manage_certificate` |
|
|
812
|
+
| `manage_certificate` | Compatibility tool for older clients; use the individual actions below | `certificates:manage` | free |
|
|
813
|
+
| `cancel_certificate` | Cancel an eligible pending certificate order | `certificates:manage` | free |
|
|
814
|
+
| `recheck_certificate_validation` | Recheck certificate validation | `certificates:manage` | free |
|
|
815
|
+
| `resend_certificate_validation` | Resend a certificate validation message after approval | `certificates:manage` | free |
|
|
816
|
+
| `change_certificate_validation` | Change the validation method for one certificate name | `certificates:manage` | free |
|
|
817
|
+
| `reissue_certificate` | Request a same-name reissue with a new public CSR | `certificates:manage` | free |
|
|
798
818
|
|
|
799
819
|
Paid certificates are portable account products: customers may install them on
|
|
800
820
|
Nginx, Apache, lighttpd, OpenLiteSpeed, HAProxy, Caddy, mail, API, load-balancer,
|
|
@@ -100,11 +100,11 @@ export const certificateAcmeSubscriptionActionRequestSchema = z.discriminatedUni
|
|
|
100
100
|
}).strict(),
|
|
101
101
|
z.object({
|
|
102
102
|
action: z.literal("remove_domain"),
|
|
103
|
-
domain_id: z.number().int().positive(),
|
|
103
|
+
domain_id: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
104
104
|
}).strict(),
|
|
105
105
|
z.object({
|
|
106
106
|
action: z.literal("replace_domain"),
|
|
107
|
-
domain_id: z.number().int().positive(),
|
|
107
|
+
domain_id: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
108
108
|
new_domain: certificateDnsIdentifierSchema,
|
|
109
109
|
}).strict(),
|
|
110
110
|
]);
|
package/build/index.js
CHANGED
|
@@ -66,7 +66,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
66
66
|
"Managed application reads require applications:read. Install and ordinary lifecycle changes require applications:manage plus an idempotencyKey and are not paid API-key operations. Selective data restore is paid and additionally requires the quote/confirm flow and applications:restore paid scope.",
|
|
67
67
|
"Application CPU, RAM, and disk figures are sizing recommendations. Do not reject an installation or filter an otherwise supported order plan because it is below those figures; product, OS, architecture, and runtime compatibility remain hard requirements.",
|
|
68
68
|
"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.",
|
|
69
|
-
"install_application and
|
|
69
|
+
"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.",
|
|
70
70
|
"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.",
|
|
71
71
|
"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.",
|
|
72
72
|
"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.",
|
|
@@ -144,14 +144,14 @@ export function createVpsnetServer(options = {}) {
|
|
|
144
144
|
"## TLS certificates and Automatic SSL",
|
|
145
145
|
"Free portable certificates are separate from paid portable certificates, paid Automatic SSL subscriptions, and application Managed HTTPS. Start with get_free_certificate_eligibility and preflight_free_certificate. Only create when preflight says issuable and the user explicitly approves the exact names, CA, validation, delivery, and key-custody mode.",
|
|
146
146
|
"For API/AI-managed deployment outside VPSnet, use customer_csr: generate and keep the private key on the destination, send only its public PKCS#10 CSR, then retrieve the public leaf and chain with download_free_certificate. In managed mode VPSnet stores an encrypted key for unattended early renewal, but that key is portal-only behind two-factor verification and is never available to MCP or API keys. Never imply that a public-only download is a deployable key pair.",
|
|
147
|
-
"Free-certificate issuance is asynchronous and quota-controlled. Poll get_free_certificate; use get_free_certificate_instruction for external-DNS CNAME delegation and download_free_certificate only after download_available=true. A queued request is not proof of issuance. Managed-key renewals are scheduled early with bounded jitter; customer_csr renewals require a new CSR. Read state before
|
|
147
|
+
"Free-certificate issuance is asynchronous and quota-controlled. Poll get_free_certificate; use get_free_certificate_instruction for external-DNS CNAME delegation and download_free_certificate only after download_available=true. A queued request is not proof of issuance. Managed-key renewals are scheduled early with bounded jitter; customer_csr renewals require a new CSR. Read state before renew_free_certificate, revoke_free_certificate, recheck_free_certificate or cancel_free_certificate; explicitly approve the requested action, and after any ambiguous response re-read state before deciding whether another action is needed.",
|
|
148
148
|
"Paid TLS certificates are portable account products, not managed-application-only features. They can be installed on customer Nginx, Apache, lighttpd, OpenLiteSpeed, HAProxy, Caddy, mail, API, load-balancer, or other TLS endpoints.",
|
|
149
149
|
"Managed HTTPS for VPSnet applications, Automatic SSL subscriptions for a customer ACME client, and portable certificate files are three separate choices. Do not place a paid order merely because an application already has managed platform HTTPS.",
|
|
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
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 manual_review is not success.",
|
|
154
|
-
"Use list_automatic_ssl_actions before
|
|
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.",
|
|
157
157
|
"Use the same client-global idempotencyKey and exact unchanged order fields for quote_certificate and order_certificate. API keys require certificates:read for reads, certificates:manage for management, and paid operations plus certificates:order and spend caps for quote/order.",
|
|
@@ -604,7 +604,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
604
604
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
605
605
|
});
|
|
606
606
|
server.registerTool("configure_application_resource_thresholds", {
|
|
607
|
-
description: "Replace the complete optional resource-threshold set and email preference for one owned managed application. Read get_application_installation first. Omitted or null numeric fields disable that threshold; omitted email_enabled is false; omitting all four thresholds clears the preferences and disables email. Thresholds highlight measured CPU, memory, combined network-per-minute, and restart-delta samples. When email is enabled, VPSnet sends one account email when a configured threshold is reached and one when it recovers, without repeating while it remains high. Thresholds do not reserve or enforce resources, restart containers, or affect billing. Confirm the exact thresholds and email preference with the user first. Requires applications:manage and is not a paid API-key operation.",
|
|
607
|
+
description: "Replace the complete optional resource-threshold set and email preference for one owned managed application. Read get_application_installation first. Omitted or null numeric fields disable that threshold; omitted email_enabled is false; omitting all four thresholds clears the preferences and disables email. Thresholds highlight measured CPU, memory, combined network-per-minute, and restart-delta samples. When email is enabled, VPSnet sends one account email when a configured threshold is reached and one when it recovers, without repeating while it remains high. Replacing thresholds can stop existing alerts. Thresholds do not reserve or enforce resources, restart containers, or affect billing. Confirm the exact thresholds and email preference with the user first. Requires applications:manage and is not a paid API-key operation.",
|
|
608
608
|
inputSchema: {
|
|
609
609
|
orderNo: applicationOrderNoSchema,
|
|
610
610
|
installation_id: applicationInstallationIdSchema,
|
|
@@ -620,8 +620,9 @@ export function createVpsnetServer(options = {}) {
|
|
|
620
620
|
annotations: {
|
|
621
621
|
title: "Configure application resource thresholds",
|
|
622
622
|
readOnlyHint: false,
|
|
623
|
-
destructiveHint:
|
|
623
|
+
destructiveHint: true,
|
|
624
624
|
idempotentHint: true,
|
|
625
|
+
openWorldHint: true,
|
|
625
626
|
},
|
|
626
627
|
}, async ({ orderNo, installation_id, cpu_percent, email_enabled, memory_mib, network_mib_per_minute, restart_delta, }) => {
|
|
627
628
|
const { data } = await apiRequest("PUT", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}/resource-thresholds`), applicationResourceThresholdRequestBody({
|
|
@@ -925,7 +926,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
925
926
|
};
|
|
926
927
|
});
|
|
927
928
|
server.registerTool("validate_application_recipe", {
|
|
928
|
-
description: "Resolve image tags 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.",
|
|
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.",
|
|
929
930
|
inputSchema: {
|
|
930
931
|
orderNo: applicationOrderNoSchema,
|
|
931
932
|
compose_yaml: customProjectComposeSchema,
|
|
@@ -936,6 +937,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
936
937
|
readOnlyHint: false,
|
|
937
938
|
destructiveHint: false,
|
|
938
939
|
idempotentHint: true,
|
|
940
|
+
openWorldHint: true,
|
|
939
941
|
},
|
|
940
942
|
}, async ({ orderNo, compose_yaml, registry_credential_ids }) => {
|
|
941
943
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
@@ -989,7 +991,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
989
991
|
};
|
|
990
992
|
});
|
|
991
993
|
server.registerTool("create_application_recipe", {
|
|
992
|
-
description: "Resolve image tags, 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.",
|
|
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.",
|
|
993
995
|
inputSchema: {
|
|
994
996
|
orderNo: applicationOrderNoSchema,
|
|
995
997
|
name: customProjectNameSchema,
|
|
@@ -1005,6 +1007,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1005
1007
|
readOnlyHint: false,
|
|
1006
1008
|
destructiveHint: false,
|
|
1007
1009
|
idempotentHint: true,
|
|
1010
|
+
openWorldHint: true,
|
|
1008
1011
|
},
|
|
1009
1012
|
}, async ({ orderNo, name, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1010
1013
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
@@ -1046,7 +1049,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1046
1049
|
};
|
|
1047
1050
|
});
|
|
1048
1051
|
server.registerTool("create_application_recipe_revision", {
|
|
1049
|
-
description: "Resolve image tags, 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.",
|
|
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.",
|
|
1050
1053
|
inputSchema: {
|
|
1051
1054
|
orderNo: applicationOrderNoSchema,
|
|
1052
1055
|
project_id: customProjectIdSchema,
|
|
@@ -1062,6 +1065,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1062
1065
|
readOnlyHint: false,
|
|
1063
1066
|
destructiveHint: false,
|
|
1064
1067
|
idempotentHint: true,
|
|
1068
|
+
openWorldHint: true,
|
|
1065
1069
|
},
|
|
1066
1070
|
}, async ({ orderNo, project_id, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
|
|
1067
1071
|
const basePath = applicationPath(orderNo, "custom-projects");
|
|
@@ -1241,6 +1245,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1241
1245
|
readOnlyHint: false,
|
|
1242
1246
|
destructiveHint: true,
|
|
1243
1247
|
idempotentHint: true,
|
|
1248
|
+
openWorldHint: true,
|
|
1244
1249
|
},
|
|
1245
1250
|
}, async ({ orderNo, adoption_id, name, env, secrets, registry_credential_ids, acknowledge_runtime_restart, idempotencyKey, }) => {
|
|
1246
1251
|
const { status, data } = await apiRequest("POST", applicationPath(orderNo, `compose-adoptions/${encodeURIComponent(adoption_id)}/confirm`), {
|
|
@@ -1320,8 +1325,9 @@ export function createVpsnetServer(options = {}) {
|
|
|
1320
1325
|
annotations: {
|
|
1321
1326
|
title: "Configure application access",
|
|
1322
1327
|
readOnlyHint: false,
|
|
1323
|
-
destructiveHint:
|
|
1328
|
+
destructiveHint: true,
|
|
1324
1329
|
idempotentHint: true,
|
|
1330
|
+
openWorldHint: true,
|
|
1325
1331
|
},
|
|
1326
1332
|
}, async ({ orderNo, installation_id, access, expected_revision, idempotencyKey, }) => {
|
|
1327
1333
|
const { status, data } = await apiRequest("POST", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}/configure-access`), applicationAccessConfigurationRequestBody(access, expected_revision), { "Idempotency-Key": idempotencyKey });
|
|
@@ -1334,107 +1340,137 @@ export function createVpsnetServer(options = {}) {
|
|
|
1334
1340
|
],
|
|
1335
1341
|
};
|
|
1336
1342
|
});
|
|
1337
|
-
|
|
1338
|
-
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.",
|
|
1339
|
-
inputSchema: {
|
|
1340
|
-
orderNo: applicationOrderNoSchema,
|
|
1341
|
-
installation_id: applicationInstallationIdSchema,
|
|
1342
|
-
action: applicationActionSchema,
|
|
1343
|
-
idempotencyKey: idempotencyKeySchema.describe("Client-global unique key. Reuse it only to replay this exact lifecycle request; never reuse it for another service or request."),
|
|
1344
|
-
expected_blueprint_version: applicationExpectedVersionSchema
|
|
1345
|
-
.optional()
|
|
1346
|
-
.describe("Required for update: exact blueprint_version from the current available_actions update capability"),
|
|
1347
|
-
expected_upstream_version: applicationExpectedVersionSchema
|
|
1348
|
-
.optional()
|
|
1349
|
-
.describe("Required for update: exact upstream_version from the current available_actions update capability"),
|
|
1350
|
-
confirmed: z
|
|
1351
|
-
.literal(true)
|
|
1352
|
-
.describe("True only after the user confirmed this lifecycle action"),
|
|
1353
|
-
acknowledge_data_loss: z
|
|
1354
|
-
.literal(true)
|
|
1355
|
-
.optional()
|
|
1356
|
-
.describe("Required for uninstall only. Set true after the user explicitly confirms permanent deletion of managed configuration, credentials, and application data."),
|
|
1357
|
-
},
|
|
1358
|
-
annotations: {
|
|
1359
|
-
title: "Manage application lifecycle",
|
|
1360
|
-
readOnlyHint: false,
|
|
1361
|
-
destructiveHint: true,
|
|
1362
|
-
idempotentHint: true,
|
|
1363
|
-
},
|
|
1364
|
-
}, async ({ orderNo, installation_id, action, idempotencyKey, expected_blueprint_version, expected_upstream_version, acknowledge_data_loss, }) => {
|
|
1343
|
+
async function performApplicationLifecycleAction({ orderNo, installation_id, action, idempotencyKey, expected_blueprint_version, expected_upstream_version, acknowledge_data_loss, }) {
|
|
1365
1344
|
if (action === "uninstall" && acknowledge_data_loss !== true) {
|
|
1366
1345
|
return {
|
|
1367
1346
|
isError: true,
|
|
1368
|
-
content: [
|
|
1369
|
-
{
|
|
1370
|
-
type: "text",
|
|
1371
|
-
text: formatJson({
|
|
1347
|
+
content: [{ type: "text", text: formatJson({
|
|
1372
1348
|
success: false,
|
|
1373
1349
|
error_codes: ["applicationUninstallConfirmationRequired"],
|
|
1374
|
-
}),
|
|
1375
|
-
},
|
|
1376
|
-
],
|
|
1350
|
+
}) }],
|
|
1377
1351
|
};
|
|
1378
1352
|
}
|
|
1379
1353
|
if (action === "update") {
|
|
1380
1354
|
if (!expected_blueprint_version || !expected_upstream_version) {
|
|
1381
1355
|
return {
|
|
1382
1356
|
isError: true,
|
|
1383
|
-
content: [{
|
|
1384
|
-
type: "text",
|
|
1385
|
-
text: formatJson({
|
|
1357
|
+
content: [{ type: "text", text: formatJson({
|
|
1386
1358
|
success: false,
|
|
1387
1359
|
error_codes: ["applicationUpdateExpectationRequired"],
|
|
1388
|
-
}),
|
|
1389
|
-
}],
|
|
1360
|
+
}) }],
|
|
1390
1361
|
};
|
|
1391
1362
|
}
|
|
1392
1363
|
const current = await apiRequest("GET", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}`));
|
|
1393
|
-
if (current.status < 200
|
|
1394
|
-
|| current.status >= 300
|
|
1364
|
+
if (current.status < 200 || current.status >= 300
|
|
1395
1365
|
|| !applicationUpdateCandidateMatches(current.data, {
|
|
1396
1366
|
blueprintVersion: expected_blueprint_version,
|
|
1397
1367
|
upstreamVersion: expected_upstream_version,
|
|
1398
1368
|
})) {
|
|
1399
1369
|
return {
|
|
1400
1370
|
isError: true,
|
|
1401
|
-
content: [{
|
|
1402
|
-
type: "text",
|
|
1403
|
-
text: formatJson({
|
|
1371
|
+
content: [{ type: "text", text: formatJson({
|
|
1404
1372
|
success: false,
|
|
1405
1373
|
error_codes: ["applicationUpdateExpectationChanged"],
|
|
1406
|
-
}),
|
|
1407
|
-
}],
|
|
1374
|
+
}) }],
|
|
1408
1375
|
};
|
|
1409
1376
|
}
|
|
1410
1377
|
}
|
|
1411
1378
|
else if (expected_blueprint_version || expected_upstream_version) {
|
|
1412
1379
|
return {
|
|
1413
1380
|
isError: true,
|
|
1414
|
-
content: [{
|
|
1415
|
-
type: "text",
|
|
1416
|
-
text: formatJson({
|
|
1381
|
+
content: [{ type: "text", text: formatJson({
|
|
1417
1382
|
success: false,
|
|
1418
1383
|
error_codes: ["applicationUpdateExpectationUnexpected"],
|
|
1419
|
-
}),
|
|
1420
|
-
}],
|
|
1384
|
+
}) }],
|
|
1421
1385
|
};
|
|
1422
1386
|
}
|
|
1423
1387
|
const { status, data } = await apiRequest("POST", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}/actions`), applicationLifecycleRequestBody(action, acknowledge_data_loss === true, action === "update"
|
|
1424
|
-
? {
|
|
1425
|
-
blueprintVersion: expected_blueprint_version,
|
|
1426
|
-
upstreamVersion: expected_upstream_version,
|
|
1427
|
-
}
|
|
1388
|
+
? { blueprintVersion: expected_blueprint_version, upstreamVersion: expected_upstream_version }
|
|
1428
1389
|
: undefined), { "Idempotency-Key": idempotencyKey });
|
|
1429
|
-
return {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1390
|
+
return { content: [{ type: "text", text: safeApplicationMutationResult(status, data, orderNo) }] };
|
|
1391
|
+
}
|
|
1392
|
+
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.",
|
|
1394
|
+
inputSchema: {
|
|
1395
|
+
orderNo: applicationOrderNoSchema,
|
|
1396
|
+
installation_id: applicationInstallationIdSchema,
|
|
1397
|
+
action: applicationActionSchema,
|
|
1398
|
+
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
|
+
expected_blueprint_version: applicationExpectedVersionSchema
|
|
1400
|
+
.optional()
|
|
1401
|
+
.describe("Required for update: exact blueprint_version from the current available_actions update capability"),
|
|
1402
|
+
expected_upstream_version: applicationExpectedVersionSchema
|
|
1403
|
+
.optional()
|
|
1404
|
+
.describe("Required for update: exact upstream_version from the current available_actions update capability"),
|
|
1405
|
+
confirmed: z
|
|
1406
|
+
.literal(true)
|
|
1407
|
+
.describe("True only after the user confirmed this lifecycle action"),
|
|
1408
|
+
acknowledge_data_loss: z
|
|
1409
|
+
.literal(true)
|
|
1410
|
+
.optional()
|
|
1411
|
+
.describe("Required for uninstall only. Set true after the user explicitly confirms permanent deletion of managed configuration, credentials, and application data."),
|
|
1412
|
+
},
|
|
1413
|
+
annotations: {
|
|
1414
|
+
title: "Manage application lifecycle",
|
|
1415
|
+
readOnlyHint: false,
|
|
1416
|
+
destructiveHint: true,
|
|
1417
|
+
idempotentHint: true,
|
|
1418
|
+
},
|
|
1419
|
+
}, performApplicationLifecycleAction);
|
|
1420
|
+
const applicationLifecycleBaseShape = {
|
|
1421
|
+
orderNo: applicationOrderNoSchema,
|
|
1422
|
+
installation_id: applicationInstallationIdSchema,
|
|
1423
|
+
idempotencyKey: idempotencyKeySchema.describe("Client-global unique key for this exact lifecycle request. Reuse only for an unchanged retry."),
|
|
1424
|
+
confirmed: z.literal(true).describe("True only after the user confirms this exact action"),
|
|
1425
|
+
};
|
|
1426
|
+
const applicationActionAnnotations = {
|
|
1427
|
+
readOnlyHint: false,
|
|
1428
|
+
destructiveHint: true,
|
|
1429
|
+
idempotentHint: true,
|
|
1430
|
+
openWorldHint: true,
|
|
1431
|
+
};
|
|
1432
|
+
server.registerTool("reconcile_application", {
|
|
1433
|
+
description: "Reconcile an owned managed application with its declared configuration. Read the installation first; confirm the action, then poll installation and events until the queued operation completes. Requires applications:manage and a client-global idempotency key.",
|
|
1434
|
+
inputSchema: applicationLifecycleBaseShape,
|
|
1435
|
+
annotations: { ...applicationActionAnnotations, title: "Reconcile application" },
|
|
1436
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "reconcile" }));
|
|
1437
|
+
server.registerTool("repair_application", {
|
|
1438
|
+
description: "Repair an owned managed application's runtime. Read current health and events first; confirm the action, then poll installation and events until the queued operation completes. Requires applications:manage and a client-global idempotency key.",
|
|
1439
|
+
inputSchema: applicationLifecycleBaseShape,
|
|
1440
|
+
annotations: { ...applicationActionAnnotations, title: "Repair application" },
|
|
1441
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "repair" }));
|
|
1442
|
+
server.registerTool("restart_application", {
|
|
1443
|
+
description: "Restart an owned managed application. This interrupts service. Confirm the interruption, then poll installation and events until the queued operation completes. Requires applications:manage and a client-global idempotency key.",
|
|
1444
|
+
inputSchema: applicationLifecycleBaseShape,
|
|
1445
|
+
annotations: { ...applicationActionAnnotations, title: "Restart application" },
|
|
1446
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "restart" }));
|
|
1447
|
+
server.registerTool("start_application", {
|
|
1448
|
+
description: "Start a stopped owned managed application. Confirm the action, then poll installation and events until the queued operation completes. Requires applications:manage and a client-global idempotency key.",
|
|
1449
|
+
inputSchema: applicationLifecycleBaseShape,
|
|
1450
|
+
annotations: { ...applicationActionAnnotations, title: "Start application", destructiveHint: false },
|
|
1451
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "start" }));
|
|
1452
|
+
server.registerTool("stop_application", {
|
|
1453
|
+
description: "Stop an owned managed application. This interrupts service. Confirm the interruption, then poll installation and events until the queued operation completes. Requires applications:manage and a client-global idempotency key.",
|
|
1454
|
+
inputSchema: applicationLifecycleBaseShape,
|
|
1455
|
+
annotations: { ...applicationActionAnnotations, title: "Stop application" },
|
|
1456
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "stop" }));
|
|
1457
|
+
server.registerTool("uninstall_application", {
|
|
1458
|
+
description: "Permanently delete an owned managed application's containers, configuration, saved credentials, and application data; server backups remain. Read the installation and obtain explicit user confirmation of data loss before calling. Poll installation and events for the final result. Requires applications:manage.",
|
|
1459
|
+
inputSchema: {
|
|
1460
|
+
...applicationLifecycleBaseShape,
|
|
1461
|
+
acknowledge_data_loss: z.literal(true).describe("The user explicitly confirmed permanent deletion of managed configuration, credentials, and application data"),
|
|
1462
|
+
},
|
|
1463
|
+
annotations: { title: "Uninstall application", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
1464
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "uninstall" }));
|
|
1465
|
+
server.registerTool("update_application", {
|
|
1466
|
+
description: "Queue the backend-selected newer immutable release for an owned managed application. First read get_application_installation and copy the exact advertised blueprint_version and upstream_version as execution preconditions; these are not caller-selected targets. Confirm the update, then poll installation and events for the final result. Requires applications:manage.",
|
|
1467
|
+
inputSchema: {
|
|
1468
|
+
...applicationLifecycleBaseShape,
|
|
1469
|
+
expected_blueprint_version: applicationExpectedVersionSchema.describe("Exact blueprint_version from the current available_actions update capability"),
|
|
1470
|
+
expected_upstream_version: applicationExpectedVersionSchema.describe("Exact upstream_version from the current available_actions update capability"),
|
|
1471
|
+
},
|
|
1472
|
+
annotations: { title: "Update application", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
1473
|
+
}, async (input) => performApplicationLifecycleAction({ ...input, action: "update" }));
|
|
1438
1474
|
server.registerTool("cancel_application_action", {
|
|
1439
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 tool re-reads the installation immediately before submitting the cancellation and refuses stale, different, or non-cancellable actions. 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.",
|
|
1440
1476
|
inputSchema: {
|
|
@@ -1446,7 +1482,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1446
1482
|
annotations: {
|
|
1447
1483
|
title: "Cancel queued application action",
|
|
1448
1484
|
readOnlyHint: false,
|
|
1449
|
-
destructiveHint:
|
|
1485
|
+
destructiveHint: true,
|
|
1450
1486
|
idempotentHint: true,
|
|
1451
1487
|
},
|
|
1452
1488
|
}, async ({ orderNo, installation_id, action_id, idempotencyKey }) => {
|
|
@@ -1584,22 +1620,24 @@ export function createVpsnetServer(options = {}) {
|
|
|
1584
1620
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1585
1621
|
});
|
|
1586
1622
|
server.registerTool("change_rdns", {
|
|
1587
|
-
description: "Set a customer PTR override for an address returned by get_rdns. Canonical length is 3-253 characters; labels are 1-63 ASCII alphanumeric/hyphen characters without leading/trailing hyphens. A trailing dot is removed and reserved VPSnet names are blocked.
|
|
1623
|
+
description: "Set a customer PTR override for an address returned by get_rdns. The new reverse DNS value replaces the previous public PTR answer after propagation. Canonical length is 3-253 characters; labels are 1-63 ASCII alphanumeric/hyphen characters without leading/trailing hyphens. A trailing dot is removed and reserved VPSnet names are blocked. OAuth requires services:manage; API keys require full access. This route is not idempotency-keyed.",
|
|
1588
1624
|
inputSchema: {
|
|
1589
1625
|
orderNo: serviceOrderNoSchema,
|
|
1590
1626
|
ip: serviceIpSchema,
|
|
1591
1627
|
value: servicePtrSchema,
|
|
1592
1628
|
},
|
|
1629
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1593
1630
|
}, async ({ orderNo, ip, value }) => {
|
|
1594
1631
|
const { data } = await apiRequest("POST", svc(orderNo, "change-rdns"), { ip, value });
|
|
1595
1632
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1596
1633
|
});
|
|
1597
1634
|
server.registerTool("clear_rdns", {
|
|
1598
|
-
description: "Clear a customer PTR override for an address returned by get_rdns. The automatic VPSnet PTR is restored, or PTR is removed when no automatic hostname exists.
|
|
1635
|
+
description: "Clear a customer PTR override for an address returned by get_rdns. The automatic VPSnet PTR is restored, or PTR is removed when no automatic hostname exists. OAuth requires services:manage; API keys require full access. This route is not idempotency-keyed.",
|
|
1599
1636
|
inputSchema: {
|
|
1600
1637
|
orderNo: serviceOrderNoSchema,
|
|
1601
1638
|
ip: serviceIpSchema,
|
|
1602
1639
|
},
|
|
1640
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1603
1641
|
}, async ({ orderNo, ip }) => {
|
|
1604
1642
|
const { data } = await apiRequest("POST", svc(orderNo, "change-rdns/clear"), { ip });
|
|
1605
1643
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -1681,7 +1719,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
1681
1719
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
1682
1720
|
});
|
|
1683
1721
|
server.registerTool("reinstall_os", {
|
|
1684
|
-
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. Returns a noty UUID for acceptance; follow get_service_history for completion. Password rules: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit.
|
|
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. 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.",
|
|
1685
1723
|
inputSchema: {
|
|
1686
1724
|
orderNo: serviceOrderNoSchema,
|
|
1687
1725
|
osVersion: z
|
|
@@ -1696,6 +1734,11 @@ export function createVpsnetServer(options = {}) {
|
|
|
1696
1734
|
.optional()
|
|
1697
1735
|
.describe("Cloud VPS and Firecracker VPS only: set true only after the user explicitly confirms deletion of the existing snapshots shown by get_os_options.snapshotDeletion. They cannot provide rollback after reinstall. Never inferred or enabled automatically."),
|
|
1698
1736
|
},
|
|
1737
|
+
annotations: {
|
|
1738
|
+
readOnlyHint: false,
|
|
1739
|
+
destructiveHint: true,
|
|
1740
|
+
idempotentHint: false,
|
|
1741
|
+
},
|
|
1699
1742
|
}, async ({ orderNo, osVersion, rootPassword, confirmSnapshotDelete }) => {
|
|
1700
1743
|
const body = { osVersion };
|
|
1701
1744
|
if (rootPassword)
|
|
@@ -2179,6 +2222,11 @@ export function createVpsnetServer(options = {}) {
|
|
|
2179
2222
|
}],
|
|
2180
2223
|
};
|
|
2181
2224
|
});
|
|
2225
|
+
async function performFreeCertificateAction(free_certificate_request_id, request) {
|
|
2226
|
+
const { action, body } = freeCertificateActionRequestBody(request);
|
|
2227
|
+
const { status, data } = await apiRequest("POST", certificatePath(`/free/${encodeURIComponent(free_certificate_request_id)}/actions/${encodeURIComponent(action)}`), body);
|
|
2228
|
+
return { content: [{ type: "text", text: formatJson(safeCertificatePayload("free-action", status, data)) }] };
|
|
2229
|
+
}
|
|
2182
2230
|
server.registerTool("manage_free_certificate", {
|
|
2183
2231
|
description: "Request one free-certificate lifecycle action after reading its current state: early renew, revoke, recheck validation, or cancel before issuance. A customer_csr renewal requires a new public CSR for the unchanged authorized names; managed-key renewal normally happens automatically. Call only after explicit user approval. If the response is ambiguous, read get_free_certificate before deciding whether another action is needed. This tool cannot export a private key. Requires certificates:manage.",
|
|
2184
2232
|
inputSchema: {
|
|
@@ -2198,15 +2246,32 @@ export function createVpsnetServer(options = {}) {
|
|
|
2198
2246
|
if (acknowledge_free_certificate_action !== true) {
|
|
2199
2247
|
throw new Error("Explicit free-certificate action approval is required.");
|
|
2200
2248
|
}
|
|
2201
|
-
|
|
2202
|
-
const { status, data } = await apiRequest("POST", certificatePath(`/free/${encodeURIComponent(free_certificate_request_id)}/actions/${encodeURIComponent(action)}`), body);
|
|
2203
|
-
return {
|
|
2204
|
-
content: [{
|
|
2205
|
-
type: "text",
|
|
2206
|
-
text: formatJson(safeCertificatePayload("free-action", status, data)),
|
|
2207
|
-
}],
|
|
2208
|
-
};
|
|
2249
|
+
return performFreeCertificateAction(free_certificate_request_id, request);
|
|
2209
2250
|
});
|
|
2251
|
+
const freeCertificateActionBaseShape = {
|
|
2252
|
+
free_certificate_request_id: freeCertificateRequestIdSchema,
|
|
2253
|
+
acknowledge_free_certificate_action: z.literal(true).describe("The user approved this exact free-certificate action"),
|
|
2254
|
+
};
|
|
2255
|
+
server.registerTool("renew_free_certificate", {
|
|
2256
|
+
description: "Request early renewal of an owned free TLS certificate after reading its current state. A customer_csr certificate requires a new public CSR for the same authorized names; managed-key certificates normally renew automatically. Approval is required. Issuance is asynchronous: poll get_free_certificate. An ambiguous response requires reading state before retrying. Requires certificates:manage; no private key is returned.",
|
|
2257
|
+
inputSchema: { ...freeCertificateActionBaseShape, csr: freeCertificateActionRequestSchema.options[0].shape.csr },
|
|
2258
|
+
annotations: { title: "Renew free TLS certificate", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2259
|
+
}, async ({ free_certificate_request_id, csr }) => performFreeCertificateAction(free_certificate_request_id, { action: "renew", csr }));
|
|
2260
|
+
server.registerTool("revoke_free_certificate", {
|
|
2261
|
+
description: "Revoke an owned free TLS certificate after checking its state and obtaining explicit user approval. Revocation invalidates the certificate; read get_free_certificate for the final state. Requires certificates:manage.",
|
|
2262
|
+
inputSchema: { ...freeCertificateActionBaseShape, reason: freeCertificateActionRequestSchema.options[1].shape.reason },
|
|
2263
|
+
annotations: { title: "Revoke free TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2264
|
+
}, async ({ free_certificate_request_id, reason }) => performFreeCertificateAction(free_certificate_request_id, { action: "revoke", reason }));
|
|
2265
|
+
server.registerTool("recheck_free_certificate", {
|
|
2266
|
+
description: "Recheck validation for an owned pending free TLS certificate after reading its current state. Approval is required; poll get_free_certificate for the final result. Requires certificates:manage.",
|
|
2267
|
+
inputSchema: freeCertificateActionBaseShape,
|
|
2268
|
+
annotations: { title: "Recheck free TLS certificate", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
2269
|
+
}, async ({ free_certificate_request_id }) => performFreeCertificateAction(free_certificate_request_id, { action: "recheck" }));
|
|
2270
|
+
server.registerTool("cancel_free_certificate", {
|
|
2271
|
+
description: "Cancel an owned pending free TLS certificate request before issuance. Read its current state and obtain explicit user approval; poll get_free_certificate for the final result. Requires certificates:manage.",
|
|
2272
|
+
inputSchema: freeCertificateActionBaseShape,
|
|
2273
|
+
annotations: { title: "Cancel free TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
2274
|
+
}, async ({ free_certificate_request_id }) => performFreeCertificateAction(free_certificate_request_id, { action: "cancel" }));
|
|
2210
2275
|
server.registerTool("list_automatic_ssl_subscriptions", {
|
|
2211
2276
|
description: "List customer-owned Automatic SSL subscriptions, subscribed DNS names, paid term, final EUR amount, public state, and whether portal-only setup credentials are ready. This never returns the private ACME server URL, account ID, EAB KID, or EAB HMAC key. Requires certificates:read.",
|
|
2212
2277
|
inputSchema: {},
|
|
@@ -2450,6 +2515,11 @@ export function createVpsnetServer(options = {}) {
|
|
|
2450
2515
|
}],
|
|
2451
2516
|
};
|
|
2452
2517
|
});
|
|
2518
|
+
async function performAutomaticSslAction(certificate_subscription_id, request, idempotencyKey) {
|
|
2519
|
+
const { action, body } = certificateAcmeSubscriptionActionRequestBody(request);
|
|
2520
|
+
const { status, data } = await apiRequest("POST", certificatePath(`/acme-subscriptions/${encodeURIComponent(certificate_subscription_id)}/actions/${encodeURIComponent(action)}`), body, { "Idempotency-Key": idempotencyKey });
|
|
2521
|
+
return { content: [{ type: "text", text: formatJson(safeCertificatePayload("acme-subscription-action", status, data)) }] };
|
|
2522
|
+
}
|
|
2453
2523
|
server.registerTool("manage_automatic_ssl_subscription", {
|
|
2454
2524
|
description: "Queue one exact idempotent Automatic SSL action: cancel the subscription, remove an eligible purchased DNS name, or replace an eligible purchased DNS name during its correction window. Read the subscription and its actions first, and call only after explicit approval. If the result becomes ambiguous, reuse the same key only for an unchanged retry or call refresh_automatic_ssl_subscription—never submit a new mutation key. Requires certificates:manage.",
|
|
2455
2525
|
inputSchema: {
|
|
@@ -2470,15 +2540,32 @@ export function createVpsnetServer(options = {}) {
|
|
|
2470
2540
|
if (acknowledge_automatic_ssl_action !== true) {
|
|
2471
2541
|
throw new Error("Explicit Automatic SSL action approval is required.");
|
|
2472
2542
|
}
|
|
2473
|
-
|
|
2474
|
-
const { status, data } = await apiRequest("POST", certificatePath(`/acme-subscriptions/${encodeURIComponent(certificate_subscription_id)}/actions/${encodeURIComponent(action)}`), body, { "Idempotency-Key": idempotencyKey });
|
|
2475
|
-
return {
|
|
2476
|
-
content: [{
|
|
2477
|
-
type: "text",
|
|
2478
|
-
text: formatJson(safeCertificatePayload("acme-subscription-action", status, data)),
|
|
2479
|
-
}],
|
|
2480
|
-
};
|
|
2543
|
+
return performAutomaticSslAction(certificate_subscription_id, request, idempotencyKey);
|
|
2481
2544
|
});
|
|
2545
|
+
const automaticSslActionBaseShape = {
|
|
2546
|
+
certificate_subscription_id: certificateSubscriptionIdSchema,
|
|
2547
|
+
acknowledge_automatic_ssl_action: z.literal(true).describe("The user approved this exact Automatic SSL action"),
|
|
2548
|
+
idempotencyKey: paidIdempotencyKeySchema.describe("Client-global key for this exact action; reuse only for an unchanged retry"),
|
|
2549
|
+
};
|
|
2550
|
+
server.registerTool("cancel_automatic_ssl_subscription", {
|
|
2551
|
+
description: "Cancel an owned Automatic SSL subscription after reading get_automatic_ssl_subscription and list_automatic_ssl_actions. Requires explicit approval. The action is durable and idempotent; for an ambiguous outcome, refresh and read state instead of using a new key. Requires certificates:manage.",
|
|
2552
|
+
inputSchema: { ...automaticSslActionBaseShape, reason: certificateAcmeSubscriptionActionRequestSchema.options[0].shape.reason },
|
|
2553
|
+
annotations: { title: "Cancel Automatic SSL subscription", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2554
|
+
}, async ({ certificate_subscription_id, reason, idempotencyKey }) => performAutomaticSslAction(certificate_subscription_id, { action: "cancel", reason }, idempotencyKey));
|
|
2555
|
+
server.registerTool("remove_automatic_ssl_domain", {
|
|
2556
|
+
description: "Remove one eligible purchased DNS name from an owned Automatic SSL subscription. Read the subscription and list_automatic_ssl_actions to verify eligibility and the exact domain ID, then obtain explicit approval. For an ambiguous outcome, refresh and read state instead of using a new key. Requires certificates:manage.",
|
|
2557
|
+
inputSchema: { ...automaticSslActionBaseShape, domain_id: certificateAcmeSubscriptionActionRequestSchema.options[1].shape.domain_id },
|
|
2558
|
+
annotations: { title: "Remove Automatic SSL domain", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2559
|
+
}, async ({ certificate_subscription_id, domain_id, idempotencyKey }) => performAutomaticSslAction(certificate_subscription_id, { action: "remove_domain", domain_id }, idempotencyKey));
|
|
2560
|
+
server.registerTool("replace_automatic_ssl_domain", {
|
|
2561
|
+
description: "Replace one eligible purchased DNS name during its Automatic SSL correction window. Read the subscription and list_automatic_ssl_actions to verify the exact domain ID and allowed replacement type, then obtain explicit approval. For an ambiguous outcome, refresh and read state instead of using a new key. Requires certificates:manage.",
|
|
2562
|
+
inputSchema: {
|
|
2563
|
+
...automaticSslActionBaseShape,
|
|
2564
|
+
domain_id: certificateAcmeSubscriptionActionRequestSchema.options[2].shape.domain_id,
|
|
2565
|
+
new_domain: certificateAcmeSubscriptionActionRequestSchema.options[2].shape.new_domain,
|
|
2566
|
+
},
|
|
2567
|
+
annotations: { title: "Replace Automatic SSL domain", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2568
|
+
}, async ({ certificate_subscription_id, domain_id, new_domain, idempotencyKey }) => performAutomaticSslAction(certificate_subscription_id, { action: "replace_domain", domain_id, new_domain }, idempotencyKey));
|
|
2482
2569
|
server.registerTool("list_certificate_catalog", {
|
|
2483
2570
|
description: "List published paid TLS certificate products and final customer offers in EUR. Products disclose validation level, public certificate-authority brand, supported names, SAN limits, management capabilities, term, and final retail prices. Only customer-visible catalog data is returned. Paid certificates are portable and are not limited to managed applications. Requires certificates:read.",
|
|
2484
2571
|
inputSchema: {},
|
|
@@ -2693,6 +2780,11 @@ export function createVpsnetServer(options = {}) {
|
|
|
2693
2780
|
}],
|
|
2694
2781
|
};
|
|
2695
2782
|
});
|
|
2783
|
+
async function performPaidCertificateAction(certificate_order_id, request, idempotencyKey) {
|
|
2784
|
+
const { action, body } = certificateActionRequestBody(request);
|
|
2785
|
+
const { status, data } = await apiRequest("POST", certificatePath(`/${encodeURIComponent(certificate_order_id)}/actions/${encodeURIComponent(action)}`), body, { "Idempotency-Key": idempotencyKey });
|
|
2786
|
+
return { content: [{ type: "text", text: formatJson(safeCertificatePayload("action", status, data)) }] };
|
|
2787
|
+
}
|
|
2696
2788
|
server.registerTool("manage_certificate", {
|
|
2697
2789
|
description: "Queue one exact idempotent paid-certificate management action: cancel an eligible pending order, recheck or resend validation, change one validation method, or request a free same-name reissue with a new public CSR. Read the product capabilities, current order, validation, and actions first. Call only after explicit approval. A reissue must preserve the paid name set and never sends a private key. If the outcome becomes ambiguous, reuse the same key only for the exact retry or call refresh_certificate—never submit a new mutation key. Requires certificates:manage.",
|
|
2698
2790
|
inputSchema: {
|
|
@@ -2713,15 +2805,47 @@ export function createVpsnetServer(options = {}) {
|
|
|
2713
2805
|
if (acknowledge_certificate_action !== true) {
|
|
2714
2806
|
throw new Error("Explicit certificate-action approval is required.");
|
|
2715
2807
|
}
|
|
2716
|
-
|
|
2717
|
-
const { status, data } = await apiRequest("POST", certificatePath(`/${encodeURIComponent(certificate_order_id)}/actions/${encodeURIComponent(action)}`), body, { "Idempotency-Key": idempotencyKey });
|
|
2718
|
-
return {
|
|
2719
|
-
content: [{
|
|
2720
|
-
type: "text",
|
|
2721
|
-
text: formatJson(safeCertificatePayload("action", status, data)),
|
|
2722
|
-
}],
|
|
2723
|
-
};
|
|
2808
|
+
return performPaidCertificateAction(certificate_order_id, request, idempotencyKey);
|
|
2724
2809
|
});
|
|
2810
|
+
const paidCertificateActionBaseShape = {
|
|
2811
|
+
certificate_order_id: certificateOrderIdSchema,
|
|
2812
|
+
acknowledge_certificate_action: z.literal(true).describe("The user approved this exact paid-certificate action"),
|
|
2813
|
+
idempotencyKey: paidIdempotencyKeySchema.describe("Client-global key for this exact action; reuse only for an unchanged retry"),
|
|
2814
|
+
};
|
|
2815
|
+
server.registerTool("cancel_certificate", {
|
|
2816
|
+
description: "Cancel an eligible pending paid TLS certificate order. Read the product capabilities, current order and list_certificate_actions, then obtain explicit approval. If the outcome is ambiguous, refresh and read state before retrying. Requires certificates:manage.",
|
|
2817
|
+
inputSchema: { ...paidCertificateActionBaseShape, reason: certificateActionRequestSchema.options[0].shape.reason },
|
|
2818
|
+
annotations: { title: "Cancel paid TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2819
|
+
}, async ({ certificate_order_id, reason, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "cancel", reason }, idempotencyKey));
|
|
2820
|
+
server.registerTool("recheck_certificate_validation", {
|
|
2821
|
+
description: "Recheck validation for the specified DNS names in an owned paid TLS certificate order. Read current validation and list_certificate_actions first, then obtain explicit approval. Poll get_certificate_validation for the final result. Requires certificates:manage.",
|
|
2822
|
+
inputSchema: { ...paidCertificateActionBaseShape, domains: certificateActionRequestSchema.options[1].shape.domains },
|
|
2823
|
+
annotations: { title: "Recheck paid certificate validation", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2824
|
+
}, async ({ certificate_order_id, domains, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "recheck_validation", domains }, idempotencyKey));
|
|
2825
|
+
server.registerTool("resend_certificate_validation", {
|
|
2826
|
+
description: "Resend validation instructions for an owned paid TLS certificate order. Read current validation and list_certificate_actions first, then obtain explicit approval. Poll get_certificate_validation for the final result. Requires certificates:manage.",
|
|
2827
|
+
inputSchema: paidCertificateActionBaseShape,
|
|
2828
|
+
annotations: { title: "Resend paid certificate validation", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2829
|
+
}, async ({ certificate_order_id, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "resend_validation" }, idempotencyKey));
|
|
2830
|
+
server.registerTool("change_certificate_validation", {
|
|
2831
|
+
description: "Change the validation method for one authorized DNS name on an owned paid TLS certificate order. Read current validation and list_certificate_actions first, then obtain explicit approval. For an ambiguous outcome, refresh and read state instead of using a new key. Requires certificates:manage.",
|
|
2832
|
+
inputSchema: {
|
|
2833
|
+
...paidCertificateActionBaseShape,
|
|
2834
|
+
domain: certificateActionRequestSchema.options[3].shape.domain,
|
|
2835
|
+
validation_method: certificateActionRequestSchema.options[3].shape.validation_method,
|
|
2836
|
+
},
|
|
2837
|
+
annotations: { title: "Change paid certificate validation", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2838
|
+
}, async ({ certificate_order_id, domain, validation_method, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "change_validation", domain, validation_method }, idempotencyKey));
|
|
2839
|
+
server.registerTool("reissue_certificate", {
|
|
2840
|
+
description: "Request a free same-name reissue of an owned paid TLS certificate using a new public CSR. Read the product, current order and list_certificate_actions; keep the paid DNS name set unchanged and obtain explicit approval. Never provide a private key. For an ambiguous outcome, refresh and read state instead of using a new key. Requires certificates:manage.",
|
|
2841
|
+
inputSchema: {
|
|
2842
|
+
...paidCertificateActionBaseShape,
|
|
2843
|
+
csr: certificateActionRequestSchema.options[4].shape.csr,
|
|
2844
|
+
common_name: certificateActionRequestSchema.options[4].shape.common_name,
|
|
2845
|
+
alternative_names: certificateActionRequestSchema.options[4].shape.alternative_names,
|
|
2846
|
+
},
|
|
2847
|
+
annotations: { title: "Reissue paid TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2848
|
+
}, async ({ certificate_order_id, csr, common_name, alternative_names, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "reissue", csr, common_name, alternative_names }, idempotencyKey));
|
|
2725
2849
|
// --- DNS ---
|
|
2726
2850
|
server.registerTool("list_domains", {
|
|
2727
2851
|
description: "List domains owned by the account, including status, expiry, nameservers, DNS zone link, and renewal settings. Requires domains:read when using an API key.",
|
|
@@ -2752,12 +2876,13 @@ export function createVpsnetServer(options = {}) {
|
|
|
2752
2876
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2753
2877
|
});
|
|
2754
2878
|
server.registerTool("set_domain_nameservers", {
|
|
2755
|
-
description: "Queue an asynchronous nameserver change for an owned domain. Nameservers must be hostnames, not IP addresses; same-domain nameserver IP records are managed separately. This does not manage DNS records or PTR. Requires domains:manage when using an API key.",
|
|
2879
|
+
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.",
|
|
2756
2880
|
inputSchema: {
|
|
2757
2881
|
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2758
2882
|
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."),
|
|
2759
2883
|
idempotencyKey: idempotencyKeySchema.optional().describe("Optional idempotency key for the queued domain action"),
|
|
2760
2884
|
},
|
|
2885
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2761
2886
|
}, async ({ domain_id, nameservers, idempotencyKey }) => {
|
|
2762
2887
|
const headers = idempotencyKey ? { "Idempotency-Key": idempotencyKey } : undefined;
|
|
2763
2888
|
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/nameservers`, {
|
|
@@ -2776,13 +2901,14 @@ export function createVpsnetServer(options = {}) {
|
|
|
2776
2901
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2777
2902
|
});
|
|
2778
2903
|
server.registerTool("create_domain_glue_record", {
|
|
2779
|
-
description: "Queue creating or updating a same-domain nameserver IP record for an owned domain. Hostname must be below the domain, addresses must be public IPs, and this does not create DNS A/AAAA records or PTR. Requires domains:manage when using an API key.",
|
|
2904
|
+
description: "Queue creating or updating a same-domain nameserver IP record at the registrar for an owned domain. The public parent-zone glue can direct resolvers to new nameserver addresses; updating an existing hostname replaces its previous addresses. Hostname must be below the domain, addresses must be public IPs, and this does not create DNS A/AAAA records or PTR. Requires domains:manage when using an API key.",
|
|
2780
2905
|
inputSchema: {
|
|
2781
2906
|
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2782
2907
|
hostname: glueHostnameSchema,
|
|
2783
2908
|
addresses: z.array(gluePublicIpSchema).min(1).max(13).describe("Public IPv4/IPv6 addresses for this nameserver host"),
|
|
2784
2909
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2785
2910
|
},
|
|
2911
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2786
2912
|
}, async ({ domain_id, hostname, addresses, idempotencyKey }) => {
|
|
2787
2913
|
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/glue`, { hostname, addresses, idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
2788
2914
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -2794,6 +2920,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2794
2920
|
record_id: positiveIdSchema.describe("Record ID from list_domain_glue_records"),
|
|
2795
2921
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2796
2922
|
},
|
|
2923
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2797
2924
|
}, async ({ domain_id, record_id, idempotencyKey }) => {
|
|
2798
2925
|
const { data } = await apiRequest("DELETE", `/account/domains/${encodeURIComponent(String(domain_id))}/glue/${encodeURIComponent(String(record_id))}`, { idempotencyKey }, { "Idempotency-Key": idempotencyKey });
|
|
2799
2926
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -2808,7 +2935,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2808
2935
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
2809
2936
|
});
|
|
2810
2937
|
server.registerTool("add_domain_parent_ds", {
|
|
2811
|
-
description: "Queue adding DNSSEC DS records at the parent zone for an owned domain. Requires domains:manage when using an API key.",
|
|
2938
|
+
description: "Queue adding public DNSSEC DS records at the parent zone for an owned domain. Confirm the records match the child zone's DNSKEY; mismatched DS records can prevent validating resolvers from resolving the domain. Requires domains:manage when using an API key.",
|
|
2812
2939
|
inputSchema: {
|
|
2813
2940
|
domain_id: positiveIdSchema.describe("Owned domain ID from list_domains"),
|
|
2814
2941
|
ds: z
|
|
@@ -2817,6 +2944,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2817
2944
|
.describe("One or more DS records, one per line. Accepted forms include '12345 13 2 ABCDEF...' or full BIND-style DS lines."),
|
|
2818
2945
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2819
2946
|
},
|
|
2947
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2820
2948
|
}, async ({ domain_id, ds, idempotencyKey }) => {
|
|
2821
2949
|
const { data } = await apiRequest("POST", `/account/domains/${encodeURIComponent(String(domain_id))}/dnssec-ds`,
|
|
2822
2950
|
// The API's preferred key is ds_records; bare `ds` is only a
|
|
@@ -2834,6 +2962,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2834
2962
|
.describe("One or more DS records to remove, one per line. Accepted forms include '12345 13 2 ABCDEF...' or full BIND-style DS lines."),
|
|
2835
2963
|
idempotencyKey: idempotencyKeySchema.describe("Unique key for this queued domain action; sent as Idempotency-Key"),
|
|
2836
2964
|
},
|
|
2965
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2837
2966
|
}, async ({ domain_id, ds, idempotencyKey }) => {
|
|
2838
2967
|
const { data } = await apiRequest("DELETE", `/account/domains/${encodeURIComponent(String(domain_id))}/dnssec-ds`,
|
|
2839
2968
|
// The API's preferred key is ds_records; bare `ds` is only a
|
|
@@ -2972,6 +3101,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2972
3101
|
quoteToken: z.string().min(32).describe("quoteToken returned by quote_domain_register"),
|
|
2973
3102
|
payment: domainPaymentSchema,
|
|
2974
3103
|
},
|
|
3104
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2975
3105
|
}, async ({ idempotencyKey, quoteToken, ...input }) => {
|
|
2976
3106
|
const body = { ...input, idempotencyKey, quoteToken };
|
|
2977
3107
|
const { data } = await apiRequest("POST", options.remoteProfile === "orders" ? "/account/mcp/orders/domains/confirm" : "/account/domains/register/confirm", body, { "Idempotency-Key": idempotencyKey, "X-Quote-Token": quoteToken });
|
|
@@ -2992,6 +3122,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2992
3122
|
quoteToken: z.string().min(32).describe("quoteToken returned by quote_domain_transfer"),
|
|
2993
3123
|
payment: domainPaymentSchema,
|
|
2994
3124
|
},
|
|
3125
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2995
3126
|
}, async ({ idempotencyKey, quoteToken, authCode, ...input }) => {
|
|
2996
3127
|
const body = { ...input, auth_code: authCode, idempotencyKey, quoteToken };
|
|
2997
3128
|
const { data } = await apiRequest("POST", "/account/domains/transfer/confirm", body, { "Idempotency-Key": idempotencyKey, "X-Quote-Token": quoteToken });
|
|
@@ -3020,6 +3151,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3020
3151
|
quoteToken: z.string().min(32).describe("quoteToken returned by quote_domain_renew"),
|
|
3021
3152
|
payment: domainPaymentSchema,
|
|
3022
3153
|
},
|
|
3154
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
3023
3155
|
}, async ({ idempotencyKey, quoteToken, ...input }) => {
|
|
3024
3156
|
const body = { ...input, idempotencyKey, quoteToken };
|
|
3025
3157
|
const { data } = await apiRequest("POST", "/account/domains/renew/confirm", body, { "Idempotency-Key": idempotencyKey, "X-Quote-Token": quoteToken });
|
|
@@ -3046,6 +3178,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3046
3178
|
quoteToken: z.string().min(32).describe("quoteToken returned by quote_domain_restore"),
|
|
3047
3179
|
payment: domainPaymentSchema,
|
|
3048
3180
|
},
|
|
3181
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
3049
3182
|
}, async ({ idempotencyKey, quoteToken, ...input }) => {
|
|
3050
3183
|
const body = { ...input, idempotencyKey, quoteToken };
|
|
3051
3184
|
const { data } = await apiRequest("POST", "/account/domains/restore/confirm", body, { "Idempotency-Key": idempotencyKey, "X-Quote-Token": quoteToken });
|
|
@@ -3102,6 +3235,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3102
3235
|
.describe("CNAME target hostname (required when type=CNAME)"),
|
|
3103
3236
|
ttl: z.number().optional().describe("TTL in seconds (default 120)"),
|
|
3104
3237
|
},
|
|
3238
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3105
3239
|
}, async ({ orderNo, zone_id, name, type, ip, target, ttl }) => {
|
|
3106
3240
|
const body = { zone_id, name, type };
|
|
3107
3241
|
if (ip !== undefined)
|
|
@@ -3540,7 +3674,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3540
3674
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3541
3675
|
});
|
|
3542
3676
|
server.registerTool("create_function", {
|
|
3543
|
-
description: "Create a Firecracker Function. Requires name, runtime_os_id, and code. Read list_functions for current runtimes and limits. Retain the idempotency key for creation recovery. Invocations are usage-billed (CPU + memory seconds) from the account balance.",
|
|
3677
|
+
description: "Create a Firecracker Function. Requires name, runtime_os_id, and code. Read list_functions for current runtimes and limits. Retain the idempotency key for creation recovery. webhook_enabled exposes a public URL for external callers. Invocations are usage-billed (CPU + memory seconds) from the account balance.",
|
|
3544
3678
|
inputSchema: {
|
|
3545
3679
|
...functionFieldsSchema,
|
|
3546
3680
|
idempotency_key: z.string().min(16).max(190).regex(/^[A-Za-z0-9._:-]+$/).optional().describe("Stable key for this exact creation. Reuse it after an uncertain response; do not create a second function."),
|
|
@@ -3551,21 +3685,27 @@ export function createVpsnetServer(options = {}) {
|
|
|
3551
3685
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3552
3686
|
});
|
|
3553
3687
|
server.registerTool("update_function", {
|
|
3554
|
-
description: "Update a Firecracker Function's code or configuration. Call get_function first. If integrity.unreadable_fields names code or environment, the backend refuses the update unless the user explicitly approves replacing every unavailable value from a trusted copy and acknowledge_unreadable_replacement=true is passed. Leave that flag false for ordinary updates.",
|
|
3688
|
+
description: "Update a Firecracker Function's code or configuration. This replaces submitted fields and can change or disable a public webhook. Call get_function first. If integrity.unreadable_fields names code or environment, the backend refuses the update unless the user explicitly approves replacing every unavailable value from a trusted copy and acknowledge_unreadable_replacement=true is passed. Leave that flag false for ordinary updates.",
|
|
3555
3689
|
inputSchema: {
|
|
3556
3690
|
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3557
3691
|
...functionFieldsSchema,
|
|
3558
3692
|
acknowledge_unreadable_replacement: z.boolean().optional().describe("Explicit customer approval to replace code or environment named by integrity.unreadable_fields; default false"),
|
|
3559
3693
|
},
|
|
3694
|
+
annotations: {
|
|
3695
|
+
readOnlyHint: false,
|
|
3696
|
+
destructiveHint: true,
|
|
3697
|
+
openWorldHint: true,
|
|
3698
|
+
},
|
|
3560
3699
|
}, async ({ function_id, ...input }) => {
|
|
3561
3700
|
const { data } = await apiRequest("POST", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}`, functionBodyFromInput(input));
|
|
3562
3701
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3563
3702
|
});
|
|
3564
3703
|
server.registerTool("delete_function", {
|
|
3565
|
-
description: "Delete a Firecracker Function.",
|
|
3704
|
+
description: "Delete a Firecracker Function, including its public webhook endpoint when enabled.",
|
|
3566
3705
|
inputSchema: {
|
|
3567
3706
|
function_id: positiveIdSchema.describe("Function ID from list_functions"),
|
|
3568
3707
|
},
|
|
3708
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3569
3709
|
}, async ({ function_id }) => {
|
|
3570
3710
|
const { data } = await apiRequest("DELETE", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}`);
|
|
3571
3711
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -3831,6 +3971,11 @@ export function createVpsnetServer(options = {}) {
|
|
|
3831
3971
|
.optional()
|
|
3832
3972
|
.describe("Required for secondary zones. Base64 TSIG secret, 32-64 decoded bytes, e.g. openssl rand -base64 32. It is encrypted and never returned."),
|
|
3833
3973
|
},
|
|
3974
|
+
annotations: {
|
|
3975
|
+
readOnlyHint: false,
|
|
3976
|
+
destructiveHint: false,
|
|
3977
|
+
openWorldHint: true,
|
|
3978
|
+
},
|
|
3834
3979
|
}, async ({ zone, kind, secondary_masters, secondary_tsig_key_name, secondary_tsig_algorithm, secondary_tsig_secret }) => {
|
|
3835
3980
|
const body = {
|
|
3836
3981
|
zone_name: zone,
|
|
@@ -3876,7 +4021,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3876
4021
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3877
4022
|
});
|
|
3878
4023
|
server.registerTool("import_dns_zone", {
|
|
3879
|
-
description: "Import BIND-style forward DNS records into a native zone. Set replace=true to replace non-system desired-state records; false upserts imported records. PTR/reverse DNS, SOA, DNSSEC wire records, apex NS, and LUA are skipped or rejected by the API. Requires dns:write when using an API key.",
|
|
4024
|
+
description: "Import BIND-style forward DNS records into a native zone. These records can change public DNS answers after publication. Set replace=true to replace non-system desired-state records; false upserts imported records. PTR/reverse DNS, SOA, DNSSEC wire records, apex NS, and LUA are skipped or rejected by the API. Requires dns:write when using an API key.",
|
|
3880
4025
|
inputSchema: {
|
|
3881
4026
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3882
4027
|
zonefile: z
|
|
@@ -3889,6 +4034,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3889
4034
|
.optional()
|
|
3890
4035
|
.describe("If true, delete existing non-system desired-state records before importing. Defaults to false."),
|
|
3891
4036
|
},
|
|
4037
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3892
4038
|
}, async ({ zone_id, zonefile, replace }) => {
|
|
3893
4039
|
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/import`, {
|
|
3894
4040
|
zonefile,
|
|
@@ -3906,7 +4052,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3906
4052
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3907
4053
|
});
|
|
3908
4054
|
server.registerTool("apply_dns_template", {
|
|
3909
|
-
description: "Apply a backend-defined DNS template to a native forward zone. Records still pass the same API validation, quotas and conflict rules as manual record writes. Prefer preview=true first to show the user exactly which records a template would write before changing the zone. Requires dns:write when using an API key.",
|
|
4055
|
+
description: "Apply a backend-defined DNS template to a native forward zone. The written records can change public DNS answers; overwrite=true replaces conflicting records. Records still pass the same API validation, quotas and conflict rules as manual record writes. Prefer preview=true first to show the user exactly which records a template would write before changing the zone. Requires dns:write when using an API key.",
|
|
3910
4056
|
inputSchema: {
|
|
3911
4057
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3912
4058
|
template: dnsTemplateIdSchema
|
|
@@ -3924,6 +4070,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3924
4070
|
.optional()
|
|
3925
4071
|
.describe("true to replace existing records that conflict with the template. Destructive: confirm the exact conflicts with the user first, ideally from a preview run."),
|
|
3926
4072
|
},
|
|
4073
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3927
4074
|
}, async ({ zone_id, template, parameters, preview, overwrite }) => {
|
|
3928
4075
|
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/templates/${encodeURIComponent(template)}`, {
|
|
3929
4076
|
...(parameters !== undefined ? { parameters } : {}),
|
|
@@ -3937,6 +4084,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3937
4084
|
inputSchema: {
|
|
3938
4085
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3939
4086
|
},
|
|
4087
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3940
4088
|
}, async ({ zone_id }) => {
|
|
3941
4089
|
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}`);
|
|
3942
4090
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -3946,6 +4094,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3946
4094
|
inputSchema: {
|
|
3947
4095
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3948
4096
|
},
|
|
4097
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3949
4098
|
}, async ({ zone_id }) => {
|
|
3950
4099
|
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/verify`);
|
|
3951
4100
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -3969,7 +4118,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3969
4118
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3970
4119
|
});
|
|
3971
4120
|
server.registerTool("set_dnssec", {
|
|
3972
|
-
description: "Enable or disable DNSSEC signing for a native DNS zone. If parent DS records still exist outside VPSNet, remove them first and set parent_ds_removed=true before disabling signing. Secondary-zone DNSSEC is controlled on the primary server. Requires dnssec:manage when using an API key.",
|
|
4121
|
+
description: "Enable or disable public DNSSEC signing for a native DNS zone. This changes the zone's DNSSEC chain and can affect validating resolvers. If parent DS records still exist outside VPSNet, remove them first and set parent_ds_removed=true before disabling signing. Secondary-zone DNSSEC is controlled on the primary server. Requires dnssec:manage when using an API key.",
|
|
3973
4122
|
inputSchema: {
|
|
3974
4123
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3975
4124
|
enabled: z.boolean().describe("true to enable/sign the zone, false to disable/remove signing"),
|
|
@@ -3978,6 +4127,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3978
4127
|
.optional()
|
|
3979
4128
|
.describe("When disabling DNSSEC for a zone whose parent DS is managed outside VPSNet, set true only after removing the parent DS records."),
|
|
3980
4129
|
},
|
|
4130
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
3981
4131
|
}, async ({ zone_id, enabled, parent_ds_removed }) => {
|
|
3982
4132
|
const { data } = await apiRequest("POST", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/dnssec`, {
|
|
3983
4133
|
enabled,
|
|
@@ -3986,7 +4136,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3986
4136
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
3987
4137
|
});
|
|
3988
4138
|
server.registerTool("upsert_dns_record", {
|
|
3989
|
-
description: "Create or replace a forward DNS record in desired state for a native zone. PTR and *.arpa are rejected; use change_rdns for reverse DNS. Requires dns:write when using an API key.",
|
|
4139
|
+
description: "Create or replace a forward DNS record in desired state for a native zone. Replacing an existing record changes the public answer after publication. PTR and *.arpa are rejected; use change_rdns for reverse DNS. Requires dns:write when using an API key.",
|
|
3990
4140
|
inputSchema: {
|
|
3991
4141
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
3992
4142
|
name: z
|
|
@@ -4004,6 +4154,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4004
4154
|
.optional()
|
|
4005
4155
|
.describe("Optional note about this record, max 255 chars."),
|
|
4006
4156
|
},
|
|
4157
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4007
4158
|
}, async ({ zone_id, name, type, content, ttl, comment }) => {
|
|
4008
4159
|
const body = { name, type, content };
|
|
4009
4160
|
if (ttl !== undefined)
|
|
@@ -4014,7 +4165,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4014
4165
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
4015
4166
|
});
|
|
4016
4167
|
server.registerTool("update_dns_record", {
|
|
4017
|
-
description: "
|
|
4168
|
+
description: "Update an existing forward DNS record by its ID (PUT), replacing its desired value, TTL or comment. A changed value can replace the public DNS answer after publication. System-managed records cannot be edited. Requires dns:write when using an API key.",
|
|
4018
4169
|
inputSchema: {
|
|
4019
4170
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4020
4171
|
record_id: positiveIdSchema.describe("DNS record ID to update"),
|
|
@@ -4033,6 +4184,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4033
4184
|
.optional()
|
|
4034
4185
|
.describe("Optional note about this record, max 255 chars."),
|
|
4035
4186
|
},
|
|
4187
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4036
4188
|
}, async ({ zone_id, record_id, name, type, content, ttl, comment }) => {
|
|
4037
4189
|
const body = { name, type, content };
|
|
4038
4190
|
if (ttl !== undefined)
|
|
@@ -4048,6 +4200,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
4048
4200
|
zone_id: positiveIdSchema.describe("DNS zone ID"),
|
|
4049
4201
|
record_id: positiveIdSchema.describe("DNS record ID"),
|
|
4050
4202
|
},
|
|
4203
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4051
4204
|
}, async ({ zone_id, record_id }) => {
|
|
4052
4205
|
const { data } = await apiRequest("DELETE", `/account/dns/zones/${encodeURIComponent(String(zone_id))}/records/${encodeURIComponent(String(record_id))}`);
|
|
4053
4206
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
@@ -109,18 +109,18 @@ export const networkingTools = [
|
|
|
109
109
|
tool("get_private_network", "GET", networks, "Read one owned network including DHCP reservations, DNS change progress, generation and project_id."),
|
|
110
110
|
tool("create_private_network", "POST", `${N}/networks`, "Create a private network intent. Every subnet is unique on the whole platform and comes from 100.64.0.0/10. Omit subnet and the platform allocates a free block of subnet_prefix_length (28..24, default 26 = 64 addresses; capabilities.private_network.subnet_sizes lists each size with its server count and suggested block); or name an aligned /28 through /24 inside 100.64.0.0/10 that no network holds (409 subnet_in_use), never both. Creation does not itself attach a server or buy a public address.", { name, subnet: z.object({ cidr: z.string().regex(/^100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])(?:\.\d{1,3}){2}\/(?:2[4-8])$/) }).strict().optional(), subnet_prefix_length: z.union([z.literal(24), z.literal(25), z.literal(26), z.literal(27), z.literal(28)]).optional().describe("Size of the allocated subnet when subnet is omitted; default 26"), settings: settings.optional(), default_policy: z.enum(["accept", "deny"]).optional() }, { idempotent: true }),
|
|
111
111
|
tool("delete_private_network", "DELETE", networks, "Request deletion of an empty network. Referenced appliances/ports may prevent deletion; quota is released only after verified cleanup.", { expected_generation: generation }, { idempotent: true, destructive: true }),
|
|
112
|
-
tool("retry_private_network", "POST", `${networks}/retry`, "Retry the failed current network operation only; preserve its current generation.
|
|
112
|
+
tool("retry_private_network", "POST", `${networks}/retry`, "Retry the failed current network operation only; preserve its current generation. It may resume a failed deletion or settings change, so inspect the failed operation before retrying.", { expected_generation: generation }, { idempotent: true, destructive: true }),
|
|
113
113
|
tool("set_private_network_dns", "POST", `${networks}/dns`, "Change DHCP DNS servers in place. Use dns_servers only for custom mode. Poll get_private_network.settings.dns_change; its DNS update UUID is not in the network operations feed.", { dns_mode: dnsMode, dns_servers: z.array(ipv4).min(1).max(3).optional() }, { idempotent: true }),
|
|
114
|
-
tool("set_private_network_names", "PUT", `${networks}/names`, "Replace the complete internal DNS zone label and alias list. Server names are automatic; these names resolve only inside this network. Omitted aliases are
|
|
114
|
+
tool("set_private_network_names", "PUT", `${networks}/names`, "Replace the complete internal DNS zone label and alias list. Server names are automatic; these names resolve only inside this network. Omitted aliases are removed and may stop resolving.", { zone_label: dnsLabel, aliases: z.array(dnsAlias).max(64) }, { destructive: true }),
|
|
115
115
|
tool("list_private_network_ports", "GET", `${networks}/ports`, "Read owned network ports, their fixed DHCP addresses, generations and attachment states. Reserved is not connected."),
|
|
116
116
|
tool("list_service_private_ports", "GET", `${N}/services/{service_id}/ports`, "Read private ports and order attachment recovery state for one owned service order number."),
|
|
117
117
|
tool("list_network_attachment_candidates", "GET", `${networks}/attachment-candidates`, "Read attachable services and each attachment_mode. Follow next_cursor using after when candidates_has_more is true; service_id selects an owned service.", { after: z.string().optional(), service_id: serviceId.optional() }, { query: ["after", "service_id"] }),
|
|
118
118
|
tool("attach_private_network_port", "POST", `${networks}/ports`, "Attach an owned service; omit ipv4 for automatic allocation or pass a fixed usable address. Check attachment-candidates first; restart=true is explicit permission for a required restart.", { service_id: serviceId, ipv4: ipv4.optional(), restart }, { idempotent: true }),
|
|
119
119
|
tool("detach_private_network_port", "DELETE", `${networks}/ports/{port_id}`, "Detach the owned server port. Use the PORT generation and confirm any required restart.", { expected_generation: generation, restart }, { idempotent: true, destructive: true }),
|
|
120
|
-
tool("retry_private_network_port", "POST", `${networks}/ports/{port_id}/retry`, "Retry the failed current port operation with the PORT generation and any required restart approval.", { expected_generation: generation, restart }, { idempotent: true }),
|
|
120
|
+
tool("retry_private_network_port", "POST", `${networks}/ports/{port_id}/retry`, "Retry the failed current port operation with the PORT generation and any required restart approval. Inspect the failed operation: a detach retry can remove the server's private connectivity.", { expected_generation: generation, restart }, { idempotent: true, destructive: true }),
|
|
121
121
|
tool("get_network_vpn_summary", "GET", `${networks}/vpn`, "Read the network's VPN summary, including truthful absence. Native site-to-site is not supported by this tool set."),
|
|
122
122
|
tool("get_network_routed_egress", "GET", `${networks}/routed-egress`, "Read this network's NAT/internet access, public address and qualification state."),
|
|
123
|
-
tool("set_network_routed_egress", "POST", `${networks}/routed-egress`, "Request the currently supported NAT transition for an owned network; the backend may refuse mode changes. public_address_id attaches an already-owned paid address and may
|
|
123
|
+
tool("set_network_routed_egress", "POST", `${networks}/routed-egress`, "Request the currently supported NAT transition for an owned network; the backend may refuse mode changes. This can replace the network's Internet route. public_address_id attaches an already-owned paid address and may remove it from a server. This never purchases an address.", { internet_access: internetAccess, expected_generation: generation, public_address_id: uuid.optional() }, { destructive: true }),
|
|
124
124
|
tool("get_security_group_capabilities", "GET", `${N}/security-groups/capabilities`, "Read security-group availability and whether the network default policy can be changed."),
|
|
125
125
|
tool("list_network_security_groups", "GET", sg, "List groups in the owned project. Copy project_id from the network; do not invent an account identifier."),
|
|
126
126
|
tool("create_network_security_group", "POST", sg, "Create a named allow-rule set. Optional network_id scopes it to that network; otherwise it belongs to the project. IPv6 rules are unavailable.", { name, rules: z.array(rule).max(256), network_id: uuid.optional() }, { idempotent: true }),
|
|
@@ -130,7 +130,7 @@ export const networkingTools = [
|
|
|
130
130
|
tool("get_network_security_policy", "GET", policy, "Read default policy, assigned groups and policy application state. Policy generation differs from network and port generations."),
|
|
131
131
|
tool("set_network_default_security_policy", "PUT", policy, "Change accept/deny for servers without custom groups. Explicit custom groups remain restrictive. Use policy generation and default-group revision from get_network_security_policy.", { default_policy: z.enum(["accept", "deny"]), expected_generation: policyGeneration, expected_default_revision: generation }, { idempotent: true, destructive: true }),
|
|
132
132
|
tool("set_private_port_security_groups", "PUT", `${project}/networks/{network_id}/ports/{port_id}/security-groups`, "Replace the port's complete customer-group list. Empty list selects the network default; at most seven customer groups fit alongside platform DNS. Use policy and port generations separately.", { group_ids: z.array(uuid).max(7), expected_generation: policyGeneration, expected_port_generation: generation }, { idempotent: true, destructive: true }),
|
|
133
|
-
tool("retry_network_security_policy", "POST", `${policy}/retry`, "Retry the failed network security policy using its current desired state.", {}, { idempotent: true }),
|
|
133
|
+
tool("retry_network_security_policy", "POST", `${policy}/retry`, "Retry the failed network security policy using its current desired state. The pending policy may replace firewall rules and remove network access; inspect its desired state first.", {}, { idempotent: true, destructive: true }),
|
|
134
134
|
tool("retire_network_security_policy", "DELETE", policy, "Retire the policy only after its server ports are gone. Removing firewall policy requires explicit user approval.", { expected_generation: generation }, { idempotent: true, destructive: true }),
|
|
135
135
|
tool("get_network_appliance_capabilities", "GET", `${A}/capabilities`, "Read available load balancer providers/tiers, public qualification, VPN capabilities and quota. Tier availability and prices are returned by the backend, never hardcoded."),
|
|
136
136
|
tool("list_network_appliances", "GET", A, "Read owned load balancers and VPN gateways with one stream cursor."),
|
package/build/remote-policy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RemoteAuthError } from './remote-auth-error.js';
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
4
|
import { networkingTools } from "./networking-tools.js";
|
|
4
5
|
import { authorizationContext } from "./request-context.js";
|
|
5
6
|
export const remoteInstructions = [
|
|
@@ -33,7 +34,7 @@ export const supportedScopes = ["services:read", "services:manage", "application
|
|
|
33
34
|
const readServices = ["get_service_rescue", "get_guest_agent_status", "get_plan_options", "get_plan_resources", "calculate_plan_change", "get_period_options", "list_restore_points", "list_restore_file_points", "browse_restore_files", "get_restore_file_browse", "list_services", "get_service", "get_service_graphs", "get_service_history", "get_hostname", "get_title", "get_rdns", "get_os_options", "get_backup_status", "get_backup_history", "list_snapshots", "list_firecracker_snapshots", "get_firecracker_backup_status", "get_firecracker_backup_history"];
|
|
34
35
|
const manageServices = ["suspend_service", "resume_service", "flush_iptables", "toggle_ipv6", "toggle_extra_settings", "reinstall_os", "rollback_snapshot", "delete_snapshot", "rollback_firecracker_snapshot", "delete_firecracker_snapshot", "start_service", "stop_service", "restart_service", "change_hostname", "reset_hostname", "change_title", "change_rdns", "clear_rdns"];
|
|
35
36
|
const readApps = ["list_application_registry_credentials", "get_application_data_restore", "list_application_catalog", "list_service_applications", "get_application_installation", "get_application_events", "get_application_health", "get_application_logs", "list_application_recipes", "list_application_recipe_revisions", "export_application_recipe", "discover_service_containers", "get_application_compose_adoption"];
|
|
36
|
-
const manageApps = ["configure_application_resource_thresholds", "validate_application_recipe", "create_application_recipe", "create_application_recipe_revision", "install_application_recipe", "prepare_application_compose_adoption", "confirm_application_compose_adoption", "install_application", "configure_application_access", "manage_application", "cancel_application_action"];
|
|
37
|
+
const manageApps = ["configure_application_resource_thresholds", "validate_application_recipe", "create_application_recipe", "create_application_recipe_revision", "install_application_recipe", "prepare_application_compose_adoption", "confirm_application_compose_adoption", "install_application", "configure_application_access", "manage_application", "reconcile_application", "repair_application", "restart_application", "start_application", "stop_application", "uninstall_application", "update_application", "cancel_application_action"];
|
|
37
38
|
const permissions = new Map([
|
|
38
39
|
["get_event", "services:read"], ["wait_for_event", "services:read"],
|
|
39
40
|
...readServices.map(n => [n, "services:read"]), ...manageServices.map(n => [n, "services:manage"]),
|
|
@@ -49,11 +50,13 @@ const permissions = new Map([
|
|
|
49
50
|
["create_free_certificate", "certificates:manage"],
|
|
50
51
|
["download_free_certificate", "certificates:read"],
|
|
51
52
|
["manage_free_certificate", "certificates:manage"],
|
|
53
|
+
...["renew_free_certificate", "revoke_free_certificate", "recheck_free_certificate", "cancel_free_certificate"].map(n => [n, "certificates:manage"]),
|
|
52
54
|
["list_automatic_ssl_subscriptions", "certificates:read"],
|
|
53
55
|
["get_automatic_ssl_subscription", "certificates:read"],
|
|
54
56
|
["list_automatic_ssl_actions", "certificates:read"],
|
|
55
57
|
["refresh_automatic_ssl_subscription", "certificates:manage"],
|
|
56
58
|
["manage_automatic_ssl_subscription", "certificates:manage"],
|
|
59
|
+
...["cancel_automatic_ssl_subscription", "remove_automatic_ssl_domain", "replace_automatic_ssl_domain"].map(n => [n, "certificates:manage"]),
|
|
57
60
|
["list_certificate_catalog", "certificates:read"],
|
|
58
61
|
["get_certificate_catalog_product", "certificates:read"],
|
|
59
62
|
["list_certificates", "certificates:read"],
|
|
@@ -63,6 +66,7 @@ const permissions = new Map([
|
|
|
63
66
|
["list_certificate_actions", "certificates:read"],
|
|
64
67
|
["refresh_certificate", "certificates:manage"],
|
|
65
68
|
["manage_certificate", "certificates:manage"],
|
|
69
|
+
...["cancel_certificate", "recheck_certificate_validation", "resend_certificate_validation", "change_certificate_validation", "reissue_certificate"].map(n => [n, "certificates:manage"]),
|
|
66
70
|
["list_domains", "domains:read"],
|
|
67
71
|
["get_domain", "domains:read"],
|
|
68
72
|
["list_domain_tlds", "domains:read"],
|
|
@@ -154,17 +158,48 @@ const additiveTools = new Set([
|
|
|
154
158
|
]);
|
|
155
159
|
const externalTools = new Set([
|
|
156
160
|
'run_vm_command', 'invoke_function', 'install_application', 'install_application_recipe',
|
|
161
|
+
'configure_application_access', 'configure_application_resource_thresholds',
|
|
162
|
+
'confirm_application_compose_adoption',
|
|
163
|
+
'create_function', 'update_function', 'delete_function', 'create_dns_zone',
|
|
164
|
+
'validate_application_recipe', 'create_application_recipe', 'create_application_recipe_revision',
|
|
157
165
|
'read_vm_file', 'write_vm_file', 'manage_application', 'get_application_health', 'get_application_logs',
|
|
166
|
+
'reconcile_application', 'repair_application', 'restart_application', 'start_application',
|
|
167
|
+
'stop_application', 'uninstall_application', 'update_application',
|
|
168
|
+
'change_rdns', 'clear_rdns', 'set_domain_nameservers',
|
|
169
|
+
'create_domain_glue_record', 'delete_domain_glue_record',
|
|
170
|
+
'add_domain_parent_ds', 'delete_domain_parent_ds', 'confirm_domain_register',
|
|
171
|
+
'attach_service_dns_record', 'import_dns_zone', 'apply_dns_template', 'delete_dns_zone',
|
|
172
|
+
'set_dnssec', 'upsert_dns_record', 'update_dns_record', 'delete_dns_record',
|
|
158
173
|
'verify_dns_zone', 'reissue_dns_zone_verification', 'get_dns_zone_diagnostics',
|
|
159
174
|
'create_free_certificate', 'manage_free_certificate', 'refresh_automatic_ssl_subscription',
|
|
160
175
|
'manage_automatic_ssl_subscription', 'refresh_certificate', 'manage_certificate',
|
|
176
|
+
'renew_free_certificate', 'revoke_free_certificate', 'recheck_free_certificate', 'cancel_free_certificate',
|
|
177
|
+
'cancel_automatic_ssl_subscription', 'remove_automatic_ssl_domain', 'replace_automatic_ssl_domain',
|
|
178
|
+
'cancel_certificate', 'recheck_certificate_validation', 'resend_certificate_validation',
|
|
179
|
+
'change_certificate_validation', 'reissue_certificate',
|
|
161
180
|
]);
|
|
162
181
|
const deduplicatedTools = new Set([
|
|
163
182
|
'run_vm_command', 'inspect_vm', 'read_vm_file', 'write_vm_file', 'create_vm_file', 'cancel_vm_operation',
|
|
164
183
|
'create_function', 'invoke_function', 'confirm_service_order', 'confirm_domain_register',
|
|
165
184
|
]);
|
|
185
|
+
// Keep cached aggregate calls valid, but advertise the individually scoped
|
|
186
|
+
// actions to new directory clients. The SDK uses one handler for tools/list;
|
|
187
|
+
// filtering its result leaves the normal tools/call dispatcher intact.
|
|
188
|
+
const legacyAggregateTools = new Set([
|
|
189
|
+
'manage_application', 'manage_free_certificate',
|
|
190
|
+
'manage_automatic_ssl_subscription', 'manage_certificate',
|
|
191
|
+
]);
|
|
166
192
|
/** Keep management tools discoverable for incremental consent; order tools require granted authority. */
|
|
167
193
|
export function installRemotePolicy(server, scopes, profile = 'directory') {
|
|
194
|
+
const setRequestHandler = server.server.setRequestHandler.bind(server.server);
|
|
195
|
+
server.server.setRequestHandler = ((schema, handler) => {
|
|
196
|
+
if (schema !== ListToolsRequestSchema)
|
|
197
|
+
return setRequestHandler(schema, handler);
|
|
198
|
+
return setRequestHandler(schema, async (...args) => {
|
|
199
|
+
const response = await handler(...args);
|
|
200
|
+
return { ...response, tools: response.tools.filter((tool) => !legacyAggregateTools.has(tool.name)) };
|
|
201
|
+
});
|
|
202
|
+
});
|
|
168
203
|
const original = server.registerTool.bind(server);
|
|
169
204
|
const register = (name, config, handler) => {
|
|
170
205
|
const required = remoteScope(name, profile);
|
|
@@ -175,13 +210,13 @@ export function installRemotePolicy(server, scopes, profile = 'directory') {
|
|
|
175
210
|
return undefined;
|
|
176
211
|
const requiredScopes = required === "authenticated" ? [] : required === "vm:session"
|
|
177
212
|
? (scopes.some(s => s.startsWith("vm:")) ? scopes.filter(s => s.startsWith("vm:")) : ["vm:diagnostics"]) : [required];
|
|
178
|
-
const readOnly =
|
|
213
|
+
const readOnly = required === "authenticated" || required.endsWith(":read") || required === "vm:diagnostics";
|
|
179
214
|
const securitySchemes = [{ type: "oauth2", scopes: requiredScopes }];
|
|
180
215
|
const annotations = {
|
|
181
216
|
readOnlyHint: readOnly, destructiveHint: !readOnly && !additiveTools.has(name),
|
|
182
217
|
idempotentHint: readOnly || deduplicatedTools.has(name),
|
|
183
|
-
...(config.annotations || {}),
|
|
184
218
|
openWorldHint: externalTools.has(name),
|
|
219
|
+
...(config.annotations || {}),
|
|
185
220
|
};
|
|
186
221
|
if (name.startsWith('quote_')) {
|
|
187
222
|
annotations.readOnlyHint = false;
|
|
@@ -193,8 +228,6 @@ export function installRemotePolicy(server, scopes, profile = 'directory') {
|
|
|
193
228
|
annotations.readOnlyHint = true;
|
|
194
229
|
annotations.destructiveHint = false;
|
|
195
230
|
}
|
|
196
|
-
if (additiveTools.has(name))
|
|
197
|
-
annotations.destructiveHint = false;
|
|
198
231
|
if (deduplicatedTools.has(name))
|
|
199
232
|
annotations.idempotentHint = true;
|
|
200
233
|
const inputSchema = { ...config.inputSchema };
|