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 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 `manage_application` with `action: "update"`, the exact
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
- `manage_application` call requires `acknowledge_data_loss=true` for uninstall,
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 `manage_automatic_ssl_subscription`;
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 `manage_application`. Omitting either is a
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` | Queue a confirmed lifecycle action, including an eligible immutable update; uninstall also requires explicit data-loss acknowledgement | `applications:manage` | free |
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` | Renew, revoke, recheck, or cancel an eligible request | `certificates:manage` | free |
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` | Cancel, remove a name, or correct an eligible name idempotently | `certificates:manage` | free |
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` | Queue an idempotent cancellation, validation, or same-name reissue action | `certificates:manage` | free |
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 manage_application are asynchronous. A queued response is not proof that the application is healthy; poll get_application_installation and inspect get_application_events.",
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 manage_free_certificate, explicitly approve revoke/cancel/renew, and after any ambiguous response re-read state before deciding whether another action is needed.",
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 manage_automatic_ssl_subscription. 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.",
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: false,
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: false,
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
- server.registerTool("manage_application", {
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
- content: [
1431
- {
1432
- type: "text",
1433
- text: safeApplicationMutationResult(status, data, orderNo),
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: false,
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. Requires services:manage and a full-access API key; this route is not idempotency-keyed.",
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. Requires services:manage and a full-access API key; this route is not idempotency-keyed.",
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. Requires services:manage and a full-access API key.",
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
- const { action, body } = freeCertificateActionRequestBody(request);
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
- const { action, body } = certificateAcmeSubscriptionActionRequestBody(request);
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
- const { action, body } = certificateActionRequestBody(request);
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: "Edit an existing forward DNS record by its ID (PUT) — change value, TTL, or comment without replacing it. System-managed records cannot be edited. Requires dns:write when using an API key.",
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. This is not a new create.", { expected_generation: generation }, { idempotent: true }),
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 not preserved.", { zone_label: dnsLabel, aliases: z.array(dnsAlias).max(64) }),
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 move it from a server. This never purchases an address.", { internet_access: internetAccess, expected_generation: generation, public_address_id: uuid.optional() }),
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."),
@@ -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 = name === 'validate_application_recipe' || required === "authenticated" || required.endsWith(":read") || required === "vm:diagnostics";
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vpsnet-mcp",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
4
4
  "type": "module",
5
5
  "description": "MCP server for managing VPSnet.com services, applications, DNS, domains, billing, and server lifecycle operations",
6
6
  "bin": {