vpsnet-mcp 2.3.4 → 2.3.6

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
 
@@ -545,13 +546,13 @@ Follow the [Windsurf MCP documentation](https://docs.windsurf.com/windsurf/mcp).
545
546
  | Variable | Required | Description |
546
547
  |----------|----------|-------------|
547
548
  | `VPSNET_API_KEY` | Yes | Your VPSnet.com API key |
548
- | `VPSNET_API_URL` | No | API base URL (defaults to `https://api.vpsnet.com`); any `https://*.vpsnet.com` host, e.g. a staging API |
549
+ | `VPSNET_API_URL` | No | API base URL (defaults to `https://api.vpsnet.com`); any `https://*.vpsnet.com` host configured for your deployment |
549
550
  | `VPSNET_AI_URL` | No | Delegated SSH runtime; production uses `https://ai.vpsnet.com`. Non-production requires the approved API origin followed by `/ai-control`. |
550
551
  | `VPSNET_AI_API_ORIGIN` | Non-production SSH only | Operator-approved HTTPS VPSnet API origin; must exactly match `VPSNET_API_URL`. Set both runtime variables privately for a non-production deployment. |
551
552
  | `VPSNET_API_TIMEOUT_MS` | No | Per-request timeout (default 45000) |
552
553
 
553
554
  Requests go out from the machine that runs this server, not from your browser. A
554
- host that admits only listed addresses (a staging API does) answers such a
555
+ deployment that restricts access by source address can answer a blocked
555
556
  request with an HTML `403` page from its web server; the tool result then says
556
557
  so (`non_api_response: true`, `http_status: 403`) instead of passing the page on.
557
558
  Every error result carries `http_status`; a `429` also carries `retry_after`.
@@ -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,
@@ -906,9 +926,9 @@ short-lived quote token.
906
926
  Customer ordering is disabled while the on-demand server lifecycle is being
907
927
  validated. Current options disclose that automatic backups and snapshots are
908
928
  not included and outbound connections to mail-delivery TCP ports 25, 2525,
909
- 465, and 587 are restricted. Existing internal test sessions remain available
910
- through list/get/delete for cleanup. Fixed-TTL and prepaid fields in the
911
- disabled compatibility schema are not a launch promise; the flexible duration
929
+ 465, and 587 are restricted. Existing sessions remain available through
930
+ list/get/delete for cleanup. Fixed-TTL and prepaid fields in the disabled
931
+ compatibility schema are not a launch promise; the flexible duration
912
932
  and metered-billing contract must be published consistently before ordering is
913
933
  enabled.
914
934
 
@@ -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.",
@@ -179,7 +179,7 @@ export function createVpsnetServer(options = {}) {
179
179
  .min(1)
180
180
  .max(64)
181
181
  .regex(/^[A-Za-z0-9-]+$/, "Use the exact service order number returned by list_services")
182
- .describe("Tenant-owned service order number: 1-64 ASCII letters, digits or hyphens, e.g. VP88318 or VP88318-1");
182
+ .describe("Tenant-owned service order number: 1-64 ASCII letters, digits or hyphens, e.g. VP12345 or VP12345-1");
183
183
  const positiveIdSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
184
184
  // Same shape OrderPrivateNetworkAdmission::admit() accepts for an order-time network.
185
185
  const privateNetworkIdSchema = z
@@ -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,7 +620,7 @@ 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
625
  },
626
626
  }, async ({ orderNo, installation_id, cpu_percent, email_enabled, memory_mib, network_mib_per_minute, restart_delta, }) => {
@@ -925,7 +925,7 @@ export function createVpsnetServer(options = {}) {
925
925
  };
926
926
  });
927
927
  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.",
928
+ 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
929
  inputSchema: {
930
930
  orderNo: applicationOrderNoSchema,
931
931
  compose_yaml: customProjectComposeSchema,
@@ -936,6 +936,7 @@ export function createVpsnetServer(options = {}) {
936
936
  readOnlyHint: false,
937
937
  destructiveHint: false,
938
938
  idempotentHint: true,
939
+ openWorldHint: true,
939
940
  },
940
941
  }, async ({ orderNo, compose_yaml, registry_credential_ids }) => {
941
942
  const basePath = applicationPath(orderNo, "custom-projects");
@@ -989,7 +990,7 @@ export function createVpsnetServer(options = {}) {
989
990
  };
990
991
  });
991
992
  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.",
993
+ 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
994
  inputSchema: {
994
995
  orderNo: applicationOrderNoSchema,
995
996
  name: customProjectNameSchema,
@@ -1005,6 +1006,7 @@ export function createVpsnetServer(options = {}) {
1005
1006
  readOnlyHint: false,
1006
1007
  destructiveHint: false,
1007
1008
  idempotentHint: true,
1009
+ openWorldHint: true,
1008
1010
  },
1009
1011
  }, async ({ orderNo, name, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
1010
1012
  const basePath = applicationPath(orderNo, "custom-projects");
@@ -1046,7 +1048,7 @@ export function createVpsnetServer(options = {}) {
1046
1048
  };
1047
1049
  });
1048
1050
  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.",
1051
+ 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
1052
  inputSchema: {
1051
1053
  orderNo: applicationOrderNoSchema,
1052
1054
  project_id: customProjectIdSchema,
@@ -1062,6 +1064,7 @@ export function createVpsnetServer(options = {}) {
1062
1064
  readOnlyHint: false,
1063
1065
  destructiveHint: false,
1064
1066
  idempotentHint: true,
1067
+ openWorldHint: true,
1065
1068
  },
1066
1069
  }, async ({ orderNo, project_id, compose_yaml, env, secret_names, registry_credential_ids, idempotencyKey, }) => {
1067
1070
  const basePath = applicationPath(orderNo, "custom-projects");
@@ -1320,7 +1323,7 @@ export function createVpsnetServer(options = {}) {
1320
1323
  annotations: {
1321
1324
  title: "Configure application access",
1322
1325
  readOnlyHint: false,
1323
- destructiveHint: false,
1326
+ destructiveHint: true,
1324
1327
  idempotentHint: true,
1325
1328
  },
1326
1329
  }, async ({ orderNo, installation_id, access, expected_revision, idempotencyKey, }) => {
@@ -1334,107 +1337,137 @@ export function createVpsnetServer(options = {}) {
1334
1337
  ],
1335
1338
  };
1336
1339
  });
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, }) => {
1340
+ async function performApplicationLifecycleAction({ orderNo, installation_id, action, idempotencyKey, expected_blueprint_version, expected_upstream_version, acknowledge_data_loss, }) {
1365
1341
  if (action === "uninstall" && acknowledge_data_loss !== true) {
1366
1342
  return {
1367
1343
  isError: true,
1368
- content: [
1369
- {
1370
- type: "text",
1371
- text: formatJson({
1344
+ content: [{ type: "text", text: formatJson({
1372
1345
  success: false,
1373
1346
  error_codes: ["applicationUninstallConfirmationRequired"],
1374
- }),
1375
- },
1376
- ],
1347
+ }) }],
1377
1348
  };
1378
1349
  }
1379
1350
  if (action === "update") {
1380
1351
  if (!expected_blueprint_version || !expected_upstream_version) {
1381
1352
  return {
1382
1353
  isError: true,
1383
- content: [{
1384
- type: "text",
1385
- text: formatJson({
1354
+ content: [{ type: "text", text: formatJson({
1386
1355
  success: false,
1387
1356
  error_codes: ["applicationUpdateExpectationRequired"],
1388
- }),
1389
- }],
1357
+ }) }],
1390
1358
  };
1391
1359
  }
1392
1360
  const current = await apiRequest("GET", applicationPath(orderNo, `installations/${encodeURIComponent(installation_id)}`));
1393
- if (current.status < 200
1394
- || current.status >= 300
1361
+ if (current.status < 200 || current.status >= 300
1395
1362
  || !applicationUpdateCandidateMatches(current.data, {
1396
1363
  blueprintVersion: expected_blueprint_version,
1397
1364
  upstreamVersion: expected_upstream_version,
1398
1365
  })) {
1399
1366
  return {
1400
1367
  isError: true,
1401
- content: [{
1402
- type: "text",
1403
- text: formatJson({
1368
+ content: [{ type: "text", text: formatJson({
1404
1369
  success: false,
1405
1370
  error_codes: ["applicationUpdateExpectationChanged"],
1406
- }),
1407
- }],
1371
+ }) }],
1408
1372
  };
1409
1373
  }
1410
1374
  }
1411
1375
  else if (expected_blueprint_version || expected_upstream_version) {
1412
1376
  return {
1413
1377
  isError: true,
1414
- content: [{
1415
- type: "text",
1416
- text: formatJson({
1378
+ content: [{ type: "text", text: formatJson({
1417
1379
  success: false,
1418
1380
  error_codes: ["applicationUpdateExpectationUnexpected"],
1419
- }),
1420
- }],
1381
+ }) }],
1421
1382
  };
1422
1383
  }
1423
1384
  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
- }
1385
+ ? { blueprintVersion: expected_blueprint_version, upstreamVersion: expected_upstream_version }
1428
1386
  : undefined), { "Idempotency-Key": idempotencyKey });
1429
- return {
1430
- content: [
1431
- {
1432
- type: "text",
1433
- text: safeApplicationMutationResult(status, data, orderNo),
1434
- },
1435
- ],
1436
- };
1437
- });
1387
+ return { content: [{ type: "text", text: safeApplicationMutationResult(status, data, orderNo) }] };
1388
+ }
1389
+ server.registerTool("manage_application", {
1390
+ 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.",
1391
+ inputSchema: {
1392
+ orderNo: applicationOrderNoSchema,
1393
+ installation_id: applicationInstallationIdSchema,
1394
+ action: applicationActionSchema,
1395
+ idempotencyKey: idempotencyKeySchema.describe("Client-global unique key. Reuse it only to replay this exact lifecycle request; never reuse it for another service or request."),
1396
+ expected_blueprint_version: applicationExpectedVersionSchema
1397
+ .optional()
1398
+ .describe("Required for update: exact blueprint_version from the current available_actions update capability"),
1399
+ expected_upstream_version: applicationExpectedVersionSchema
1400
+ .optional()
1401
+ .describe("Required for update: exact upstream_version from the current available_actions update capability"),
1402
+ confirmed: z
1403
+ .literal(true)
1404
+ .describe("True only after the user confirmed this lifecycle action"),
1405
+ acknowledge_data_loss: z
1406
+ .literal(true)
1407
+ .optional()
1408
+ .describe("Required for uninstall only. Set true after the user explicitly confirms permanent deletion of managed configuration, credentials, and application data."),
1409
+ },
1410
+ annotations: {
1411
+ title: "Manage application lifecycle",
1412
+ readOnlyHint: false,
1413
+ destructiveHint: true,
1414
+ idempotentHint: true,
1415
+ },
1416
+ }, performApplicationLifecycleAction);
1417
+ const applicationLifecycleBaseShape = {
1418
+ orderNo: applicationOrderNoSchema,
1419
+ installation_id: applicationInstallationIdSchema,
1420
+ idempotencyKey: idempotencyKeySchema.describe("Client-global unique key for this exact lifecycle request. Reuse only for an unchanged retry."),
1421
+ confirmed: z.literal(true).describe("True only after the user confirms this exact action"),
1422
+ };
1423
+ const applicationActionAnnotations = {
1424
+ readOnlyHint: false,
1425
+ destructiveHint: true,
1426
+ idempotentHint: true,
1427
+ openWorldHint: true,
1428
+ };
1429
+ server.registerTool("reconcile_application", {
1430
+ 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.",
1431
+ inputSchema: applicationLifecycleBaseShape,
1432
+ annotations: { ...applicationActionAnnotations, title: "Reconcile application" },
1433
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "reconcile" }));
1434
+ server.registerTool("repair_application", {
1435
+ 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.",
1436
+ inputSchema: applicationLifecycleBaseShape,
1437
+ annotations: { ...applicationActionAnnotations, title: "Repair application" },
1438
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "repair" }));
1439
+ server.registerTool("restart_application", {
1440
+ 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.",
1441
+ inputSchema: applicationLifecycleBaseShape,
1442
+ annotations: { ...applicationActionAnnotations, title: "Restart application" },
1443
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "restart" }));
1444
+ server.registerTool("start_application", {
1445
+ 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.",
1446
+ inputSchema: applicationLifecycleBaseShape,
1447
+ annotations: { ...applicationActionAnnotations, title: "Start application", destructiveHint: false },
1448
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "start" }));
1449
+ server.registerTool("stop_application", {
1450
+ 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.",
1451
+ inputSchema: applicationLifecycleBaseShape,
1452
+ annotations: { ...applicationActionAnnotations, title: "Stop application" },
1453
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "stop" }));
1454
+ server.registerTool("uninstall_application", {
1455
+ 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.",
1456
+ inputSchema: {
1457
+ ...applicationLifecycleBaseShape,
1458
+ acknowledge_data_loss: z.literal(true).describe("The user explicitly confirmed permanent deletion of managed configuration, credentials, and application data"),
1459
+ },
1460
+ annotations: { title: "Uninstall application", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
1461
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "uninstall" }));
1462
+ server.registerTool("update_application", {
1463
+ 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.",
1464
+ inputSchema: {
1465
+ ...applicationLifecycleBaseShape,
1466
+ expected_blueprint_version: applicationExpectedVersionSchema.describe("Exact blueprint_version from the current available_actions update capability"),
1467
+ expected_upstream_version: applicationExpectedVersionSchema.describe("Exact upstream_version from the current available_actions update capability"),
1468
+ },
1469
+ annotations: { title: "Update application", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
1470
+ }, async (input) => performApplicationLifecycleAction({ ...input, action: "update" }));
1438
1471
  server.registerTool("cancel_application_action", {
1439
1472
  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
1473
  inputSchema: {
@@ -1446,7 +1479,7 @@ export function createVpsnetServer(options = {}) {
1446
1479
  annotations: {
1447
1480
  title: "Cancel queued application action",
1448
1481
  readOnlyHint: false,
1449
- destructiveHint: false,
1482
+ destructiveHint: true,
1450
1483
  idempotentHint: true,
1451
1484
  },
1452
1485
  }, async ({ orderNo, installation_id, action_id, idempotencyKey }) => {
@@ -1681,7 +1714,7 @@ export function createVpsnetServer(options = {}) {
1681
1714
  return { content: [{ type: "text", text: formatJson(data) }] };
1682
1715
  });
1683
1716
  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.",
1717
+ 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
1718
  inputSchema: {
1686
1719
  orderNo: serviceOrderNoSchema,
1687
1720
  osVersion: z
@@ -1696,6 +1729,11 @@ export function createVpsnetServer(options = {}) {
1696
1729
  .optional()
1697
1730
  .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
1731
  },
1732
+ annotations: {
1733
+ readOnlyHint: false,
1734
+ destructiveHint: true,
1735
+ idempotentHint: false,
1736
+ },
1699
1737
  }, async ({ orderNo, osVersion, rootPassword, confirmSnapshotDelete }) => {
1700
1738
  const body = { osVersion };
1701
1739
  if (rootPassword)
@@ -2179,6 +2217,11 @@ export function createVpsnetServer(options = {}) {
2179
2217
  }],
2180
2218
  };
2181
2219
  });
2220
+ async function performFreeCertificateAction(free_certificate_request_id, request) {
2221
+ const { action, body } = freeCertificateActionRequestBody(request);
2222
+ const { status, data } = await apiRequest("POST", certificatePath(`/free/${encodeURIComponent(free_certificate_request_id)}/actions/${encodeURIComponent(action)}`), body);
2223
+ return { content: [{ type: "text", text: formatJson(safeCertificatePayload("free-action", status, data)) }] };
2224
+ }
2182
2225
  server.registerTool("manage_free_certificate", {
2183
2226
  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
2227
  inputSchema: {
@@ -2198,15 +2241,32 @@ export function createVpsnetServer(options = {}) {
2198
2241
  if (acknowledge_free_certificate_action !== true) {
2199
2242
  throw new Error("Explicit free-certificate action approval is required.");
2200
2243
  }
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
- };
2244
+ return performFreeCertificateAction(free_certificate_request_id, request);
2209
2245
  });
2246
+ const freeCertificateActionBaseShape = {
2247
+ free_certificate_request_id: freeCertificateRequestIdSchema,
2248
+ acknowledge_free_certificate_action: z.literal(true).describe("The user approved this exact free-certificate action"),
2249
+ };
2250
+ server.registerTool("renew_free_certificate", {
2251
+ 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.",
2252
+ inputSchema: { ...freeCertificateActionBaseShape, csr: freeCertificateActionRequestSchema.options[0].shape.csr },
2253
+ annotations: { title: "Renew free TLS certificate", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2254
+ }, async ({ free_certificate_request_id, csr }) => performFreeCertificateAction(free_certificate_request_id, { action: "renew", csr }));
2255
+ server.registerTool("revoke_free_certificate", {
2256
+ 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.",
2257
+ inputSchema: { ...freeCertificateActionBaseShape, reason: freeCertificateActionRequestSchema.options[1].shape.reason },
2258
+ annotations: { title: "Revoke free TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
2259
+ }, async ({ free_certificate_request_id, reason }) => performFreeCertificateAction(free_certificate_request_id, { action: "revoke", reason }));
2260
+ server.registerTool("recheck_free_certificate", {
2261
+ 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.",
2262
+ inputSchema: freeCertificateActionBaseShape,
2263
+ annotations: { title: "Recheck free TLS certificate", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2264
+ }, async ({ free_certificate_request_id }) => performFreeCertificateAction(free_certificate_request_id, { action: "recheck" }));
2265
+ server.registerTool("cancel_free_certificate", {
2266
+ 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.",
2267
+ inputSchema: freeCertificateActionBaseShape,
2268
+ annotations: { title: "Cancel free TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
2269
+ }, async ({ free_certificate_request_id }) => performFreeCertificateAction(free_certificate_request_id, { action: "cancel" }));
2210
2270
  server.registerTool("list_automatic_ssl_subscriptions", {
2211
2271
  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
2272
  inputSchema: {},
@@ -2450,6 +2510,11 @@ export function createVpsnetServer(options = {}) {
2450
2510
  }],
2451
2511
  };
2452
2512
  });
2513
+ async function performAutomaticSslAction(certificate_subscription_id, request, idempotencyKey) {
2514
+ const { action, body } = certificateAcmeSubscriptionActionRequestBody(request);
2515
+ const { status, data } = await apiRequest("POST", certificatePath(`/acme-subscriptions/${encodeURIComponent(certificate_subscription_id)}/actions/${encodeURIComponent(action)}`), body, { "Idempotency-Key": idempotencyKey });
2516
+ return { content: [{ type: "text", text: formatJson(safeCertificatePayload("acme-subscription-action", status, data)) }] };
2517
+ }
2453
2518
  server.registerTool("manage_automatic_ssl_subscription", {
2454
2519
  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
2520
  inputSchema: {
@@ -2470,15 +2535,32 @@ export function createVpsnetServer(options = {}) {
2470
2535
  if (acknowledge_automatic_ssl_action !== true) {
2471
2536
  throw new Error("Explicit Automatic SSL action approval is required.");
2472
2537
  }
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
- };
2538
+ return performAutomaticSslAction(certificate_subscription_id, request, idempotencyKey);
2481
2539
  });
2540
+ const automaticSslActionBaseShape = {
2541
+ certificate_subscription_id: certificateSubscriptionIdSchema,
2542
+ acknowledge_automatic_ssl_action: z.literal(true).describe("The user approved this exact Automatic SSL action"),
2543
+ idempotencyKey: paidIdempotencyKeySchema.describe("Client-global key for this exact action; reuse only for an unchanged retry"),
2544
+ };
2545
+ server.registerTool("cancel_automatic_ssl_subscription", {
2546
+ 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.",
2547
+ inputSchema: { ...automaticSslActionBaseShape, reason: certificateAcmeSubscriptionActionRequestSchema.options[0].shape.reason },
2548
+ annotations: { title: "Cancel Automatic SSL subscription", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2549
+ }, async ({ certificate_subscription_id, reason, idempotencyKey }) => performAutomaticSslAction(certificate_subscription_id, { action: "cancel", reason }, idempotencyKey));
2550
+ server.registerTool("remove_automatic_ssl_domain", {
2551
+ 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.",
2552
+ inputSchema: { ...automaticSslActionBaseShape, domain_id: certificateAcmeSubscriptionActionRequestSchema.options[1].shape.domain_id },
2553
+ annotations: { title: "Remove Automatic SSL domain", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2554
+ }, async ({ certificate_subscription_id, domain_id, idempotencyKey }) => performAutomaticSslAction(certificate_subscription_id, { action: "remove_domain", domain_id }, idempotencyKey));
2555
+ server.registerTool("replace_automatic_ssl_domain", {
2556
+ 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.",
2557
+ inputSchema: {
2558
+ ...automaticSslActionBaseShape,
2559
+ domain_id: certificateAcmeSubscriptionActionRequestSchema.options[2].shape.domain_id,
2560
+ new_domain: certificateAcmeSubscriptionActionRequestSchema.options[2].shape.new_domain,
2561
+ },
2562
+ annotations: { title: "Replace Automatic SSL domain", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2563
+ }, async ({ certificate_subscription_id, domain_id, new_domain, idempotencyKey }) => performAutomaticSslAction(certificate_subscription_id, { action: "replace_domain", domain_id, new_domain }, idempotencyKey));
2482
2564
  server.registerTool("list_certificate_catalog", {
2483
2565
  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
2566
  inputSchema: {},
@@ -2693,6 +2775,11 @@ export function createVpsnetServer(options = {}) {
2693
2775
  }],
2694
2776
  };
2695
2777
  });
2778
+ async function performPaidCertificateAction(certificate_order_id, request, idempotencyKey) {
2779
+ const { action, body } = certificateActionRequestBody(request);
2780
+ const { status, data } = await apiRequest("POST", certificatePath(`/${encodeURIComponent(certificate_order_id)}/actions/${encodeURIComponent(action)}`), body, { "Idempotency-Key": idempotencyKey });
2781
+ return { content: [{ type: "text", text: formatJson(safeCertificatePayload("action", status, data)) }] };
2782
+ }
2696
2783
  server.registerTool("manage_certificate", {
2697
2784
  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
2785
  inputSchema: {
@@ -2713,15 +2800,47 @@ export function createVpsnetServer(options = {}) {
2713
2800
  if (acknowledge_certificate_action !== true) {
2714
2801
  throw new Error("Explicit certificate-action approval is required.");
2715
2802
  }
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
- };
2803
+ return performPaidCertificateAction(certificate_order_id, request, idempotencyKey);
2724
2804
  });
2805
+ const paidCertificateActionBaseShape = {
2806
+ certificate_order_id: certificateOrderIdSchema,
2807
+ acknowledge_certificate_action: z.literal(true).describe("The user approved this exact paid-certificate action"),
2808
+ idempotencyKey: paidIdempotencyKeySchema.describe("Client-global key for this exact action; reuse only for an unchanged retry"),
2809
+ };
2810
+ server.registerTool("cancel_certificate", {
2811
+ 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.",
2812
+ inputSchema: { ...paidCertificateActionBaseShape, reason: certificateActionRequestSchema.options[0].shape.reason },
2813
+ annotations: { title: "Cancel paid TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2814
+ }, async ({ certificate_order_id, reason, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "cancel", reason }, idempotencyKey));
2815
+ server.registerTool("recheck_certificate_validation", {
2816
+ 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.",
2817
+ inputSchema: { ...paidCertificateActionBaseShape, domains: certificateActionRequestSchema.options[1].shape.domains },
2818
+ annotations: { title: "Recheck paid certificate validation", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2819
+ }, async ({ certificate_order_id, domains, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "recheck_validation", domains }, idempotencyKey));
2820
+ server.registerTool("resend_certificate_validation", {
2821
+ 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.",
2822
+ inputSchema: paidCertificateActionBaseShape,
2823
+ annotations: { title: "Resend paid certificate validation", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2824
+ }, async ({ certificate_order_id, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "resend_validation" }, idempotencyKey));
2825
+ server.registerTool("change_certificate_validation", {
2826
+ 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.",
2827
+ inputSchema: {
2828
+ ...paidCertificateActionBaseShape,
2829
+ domain: certificateActionRequestSchema.options[3].shape.domain,
2830
+ validation_method: certificateActionRequestSchema.options[3].shape.validation_method,
2831
+ },
2832
+ annotations: { title: "Change paid certificate validation", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2833
+ }, async ({ certificate_order_id, domain, validation_method, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "change_validation", domain, validation_method }, idempotencyKey));
2834
+ server.registerTool("reissue_certificate", {
2835
+ 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.",
2836
+ inputSchema: {
2837
+ ...paidCertificateActionBaseShape,
2838
+ csr: certificateActionRequestSchema.options[4].shape.csr,
2839
+ common_name: certificateActionRequestSchema.options[4].shape.common_name,
2840
+ alternative_names: certificateActionRequestSchema.options[4].shape.alternative_names,
2841
+ },
2842
+ annotations: { title: "Reissue paid TLS certificate", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2843
+ }, async ({ certificate_order_id, csr, common_name, alternative_names, idempotencyKey }) => performPaidCertificateAction(certificate_order_id, { action: "reissue", csr, common_name, alternative_names }, idempotencyKey));
2725
2844
  // --- DNS ---
2726
2845
  server.registerTool("list_domains", {
2727
2846
  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.",
@@ -3540,7 +3659,7 @@ export function createVpsnetServer(options = {}) {
3540
3659
  return { content: [{ type: "text", text: formatJson(data) }] };
3541
3660
  });
3542
3661
  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.",
3662
+ 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
3663
  inputSchema: {
3545
3664
  ...functionFieldsSchema,
3546
3665
  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,12 +3670,17 @@ export function createVpsnetServer(options = {}) {
3551
3670
  return { content: [{ type: "text", text: formatJson(data) }] };
3552
3671
  });
3553
3672
  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.",
3673
+ 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
3674
  inputSchema: {
3556
3675
  function_id: positiveIdSchema.describe("Function ID from list_functions"),
3557
3676
  ...functionFieldsSchema,
3558
3677
  acknowledge_unreadable_replacement: z.boolean().optional().describe("Explicit customer approval to replace code or environment named by integrity.unreadable_fields; default false"),
3559
3678
  },
3679
+ annotations: {
3680
+ readOnlyHint: false,
3681
+ destructiveHint: true,
3682
+ openWorldHint: true,
3683
+ },
3560
3684
  }, async ({ function_id, ...input }) => {
3561
3685
  const { data } = await apiRequest("POST", `/account/firecracker/functions/${encodeURIComponent(String(function_id))}`, functionBodyFromInput(input));
3562
3686
  return { content: [{ type: "text", text: formatJson(data) }] };
@@ -3831,6 +3955,11 @@ export function createVpsnetServer(options = {}) {
3831
3955
  .optional()
3832
3956
  .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
3957
  },
3958
+ annotations: {
3959
+ readOnlyHint: false,
3960
+ destructiveHint: false,
3961
+ openWorldHint: true,
3962
+ },
3834
3963
  }, async ({ zone, kind, secondary_masters, secondary_tsig_key_name, secondary_tsig_algorithm, secondary_tsig_secret }) => {
3835
3964
  const body = {
3836
3965
  zone_name: zone,
@@ -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,41 @@ 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
+ 'create_function', 'update_function', 'create_dns_zone',
162
+ 'validate_application_recipe', 'create_application_recipe', 'create_application_recipe_revision',
157
163
  'read_vm_file', 'write_vm_file', 'manage_application', 'get_application_health', 'get_application_logs',
164
+ 'reconcile_application', 'repair_application', 'restart_application', 'start_application',
165
+ 'stop_application', 'uninstall_application', 'update_application',
158
166
  'verify_dns_zone', 'reissue_dns_zone_verification', 'get_dns_zone_diagnostics',
159
167
  'create_free_certificate', 'manage_free_certificate', 'refresh_automatic_ssl_subscription',
160
168
  'manage_automatic_ssl_subscription', 'refresh_certificate', 'manage_certificate',
169
+ 'renew_free_certificate', 'revoke_free_certificate', 'recheck_free_certificate', 'cancel_free_certificate',
170
+ 'cancel_automatic_ssl_subscription', 'remove_automatic_ssl_domain', 'replace_automatic_ssl_domain',
171
+ 'cancel_certificate', 'recheck_certificate_validation', 'resend_certificate_validation',
172
+ 'change_certificate_validation', 'reissue_certificate',
161
173
  ]);
162
174
  const deduplicatedTools = new Set([
163
175
  'run_vm_command', 'inspect_vm', 'read_vm_file', 'write_vm_file', 'create_vm_file', 'cancel_vm_operation',
164
176
  'create_function', 'invoke_function', 'confirm_service_order', 'confirm_domain_register',
165
177
  ]);
178
+ // Keep cached aggregate calls valid, but advertise the individually scoped
179
+ // actions to new directory clients. The SDK uses one handler for tools/list;
180
+ // filtering its result leaves the normal tools/call dispatcher intact.
181
+ const legacyAggregateTools = new Set([
182
+ 'manage_application', 'manage_free_certificate',
183
+ 'manage_automatic_ssl_subscription', 'manage_certificate',
184
+ ]);
166
185
  /** Keep management tools discoverable for incremental consent; order tools require granted authority. */
167
186
  export function installRemotePolicy(server, scopes, profile = 'directory') {
187
+ const setRequestHandler = server.server.setRequestHandler.bind(server.server);
188
+ server.server.setRequestHandler = ((schema, handler) => {
189
+ if (schema !== ListToolsRequestSchema)
190
+ return setRequestHandler(schema, handler);
191
+ return setRequestHandler(schema, async (...args) => {
192
+ const response = await handler(...args);
193
+ return { ...response, tools: response.tools.filter((tool) => !legacyAggregateTools.has(tool.name)) };
194
+ });
195
+ });
168
196
  const original = server.registerTool.bind(server);
169
197
  const register = (name, config, handler) => {
170
198
  const required = remoteScope(name, profile);
@@ -175,13 +203,13 @@ export function installRemotePolicy(server, scopes, profile = 'directory') {
175
203
  return undefined;
176
204
  const requiredScopes = required === "authenticated" ? [] : required === "vm:session"
177
205
  ? (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";
206
+ const readOnly = required === "authenticated" || required.endsWith(":read") || required === "vm:diagnostics";
179
207
  const securitySchemes = [{ type: "oauth2", scopes: requiredScopes }];
180
208
  const annotations = {
181
209
  readOnlyHint: readOnly, destructiveHint: !readOnly && !additiveTools.has(name),
182
210
  idempotentHint: readOnly || deduplicatedTools.has(name),
183
- ...(config.annotations || {}),
184
211
  openWorldHint: externalTools.has(name),
212
+ ...(config.annotations || {}),
185
213
  };
186
214
  if (name.startsWith('quote_')) {
187
215
  annotations.readOnlyHint = false;
@@ -193,8 +221,6 @@ export function installRemotePolicy(server, scopes, profile = 'directory') {
193
221
  annotations.readOnlyHint = true;
194
222
  annotations.destructiveHint = false;
195
223
  }
196
- if (additiveTools.has(name))
197
- annotations.destructiveHint = false;
198
224
  if (deduplicatedTools.has(name))
199
225
  annotations.idempotentHint = true;
200
226
  const inputSchema = { ...config.inputSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vpsnet-mcp",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
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": {