@sakupa/mcp 0.7.19 → 0.7.20

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.
Files changed (3) hide show
  1. package/dist/bin.js +41 -11
  2. package/dist/index.js +41 -11
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
129
129
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
130
130
 
131
131
  // ../core/dist/domain/version.js
132
- var SAKUPA_MCP_VERSION = "0.7.19";
132
+ var SAKUPA_MCP_VERSION = "0.7.20";
133
133
 
134
134
  // ../core/dist/domain/errors.js
135
135
  var HTTP_STATUS = {
@@ -455,6 +455,9 @@ var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
455
455
  // ../core/dist/services/subscriptions.js
456
456
  var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
457
457
 
458
+ // ../core/dist/services/lifecycle.js
459
+ var EPHEMERAL_RETENTION_HOURS = 90 * 24;
460
+
458
461
  // src/config.ts
459
462
  var TEST_API_BASE_URL = "https://api-test.sakupa.com";
460
463
  function previewHostPatternFor(apiBaseUrl) {
@@ -1351,8 +1354,8 @@ async function diagnoseBinding(input) {
1351
1354
  const apexResolves = apexAnswers.length > 0;
1352
1355
  const allOk = checks.every((c) => c.state === "ok");
1353
1356
  const checklist = checks.map(renderCheck).join("\n") + `
1354
- [${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most DNS panels accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
1355
- const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Sakupa's certificate service validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
1357
+ [${apexResolves ? "OK" : "OPTIONAL"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves; this optional extra entrance is available." : `not configured, which is valid and does NOT block binding. If the user explicitly wants the naked domain too, use the first option their DNS provider supports: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most DNS panels accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) Otherwise leave the apex unconfigured or add a URL redirect from @ to www; never treat this as an error.`);
1358
+ const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Sakupa's certificate service validates and issues within minutes once the required www record above is OK; run bind status again in about 5 minutes to re-check the provider and advance activation)." : "starts after verification.");
1356
1359
  return { checks, apexResolves, allOk, checklist, layers };
1357
1360
  }
1358
1361
  var DNS_RETRY_AFTER_SECONDS = 300;
@@ -1373,7 +1376,7 @@ ${diag.checklist}
1373
1376
 
1374
1377
  ${diag.layers}
1375
1378
 
1376
- ` + (diag.allOk ? "All records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
1379
+ ` + (diag.allOk ? "All required records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
1377
1380
  }
1378
1381
 
1379
1382
  // src/version.ts
@@ -2018,7 +2021,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2018
2021
  server.registerTool(
2019
2022
  "bind",
2020
2023
  {
2021
- description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
2024
+ description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. One apex TXT verification covers both, and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
2022
2025
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2023
2026
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
2024
2027
  inputSchema: {
@@ -2036,7 +2039,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2036
2039
  const site = requireSiteFile(ctx);
2037
2040
  if (args.action === "status") {
2038
2041
  const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
2039
- if (res2.status === "verified") {
2042
+ if (res2.bindingStatus === "active") {
2040
2043
  writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
2041
2044
  }
2042
2045
  const apex2 = res2.apexDomain;
@@ -2046,11 +2049,11 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2046
2049
  ...res2.verificationRecord ? { verificationRecord: res2.verificationRecord } : {},
2047
2050
  pendingDnsRecords: res2.pendingDnsRecords,
2048
2051
  verificationStatus: res2.status,
2049
- provisioning: res2.provisioningJobId !== void 0
2052
+ provisioning: res2.bindingStatus === "provisioning"
2050
2053
  });
2051
2054
  const { apexResolves, allOk } = diag;
2052
2055
  return text(
2053
- res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
2056
+ res2.bindingStatus === "active" ? "domain_binding_active" : res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
2054
2057
  `Domain binding status for ${apex2}: ${res2.status}
2055
2058
  ${res2.message}
2056
2059
 
@@ -2061,16 +2064,20 @@ ${res2.message}
2061
2064
  {
2062
2065
  verificationId: res2.verificationId,
2063
2066
  status: res2.status,
2067
+ bindingStatus: res2.bindingStatus,
2064
2068
  apexDomain: apex2,
2065
2069
  provisioningJobId: res2.provisioningJobId,
2066
2070
  servingTarget: res2.servingTarget,
2067
2071
  dnsChecklist: toDnsChecklist(diag.checks),
2068
2072
  apexResolves,
2073
+ apexRequired: false,
2074
+ requiredServingHostname: `www.${apex2}`,
2075
+ optionalApexHostname: apex2,
2069
2076
  retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
2070
2077
  maxAttempts: DNS_MAX_ATTEMPTS,
2071
2078
  message: res2.message
2072
2079
  },
2073
- res2.status === "verified" ? "pending_provider" : "waiting_user"
2080
+ res2.bindingStatus === "active" ? "completed" : res2.status === "verified" ? "pending_provider" : "waiting_user"
2074
2081
  );
2075
2082
  }
2076
2083
  if (!args.hostname) {
@@ -2088,7 +2095,7 @@ ${res2.message}
2088
2095
  ` : "";
2089
2096
  return text(
2090
2097
  "domain_verification_started",
2091
- switchNotice + `Domain binding started for ${apex} (will serve: ${res.includedHostnames.join(", ")}).
2098
+ switchNotice + `Domain binding started for ${apex} (routes reserved: ${res.includedHostnames.join(", ")}). Only www.${apex} is required to go live; the naked domain is optional.
2092
2099
 
2093
2100
  This is a STEP-BY-STEP setup \u2014 give the user ONE record at a time so they do not get overwhelmed and give up.
2094
2101
 
@@ -2103,6 +2110,9 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
2103
2110
  verificationId: res.verificationId,
2104
2111
  apexDomain: apex,
2105
2112
  includedHostnames: res.includedHostnames,
2113
+ apexRequired: false,
2114
+ requiredServingHostname: `www.${apex}`,
2115
+ optionalApexHostname: apex,
2106
2116
  verificationRecord: res.verificationRecord,
2107
2117
  servingTarget: res.servingTarget,
2108
2118
  ...res.supersedes ? { supersedes: res.supersedes } : {},
@@ -2523,7 +2533,7 @@ function registerLifecycleTools(server, baseCtx) {
2523
2533
  server.registerTool(
2524
2534
  "delete",
2525
2535
  {
2526
- description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
2536
+ description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state. Paid sites must first cancel renewal through portal and return to free mode. For a temporary pause, publish a pause notice as index.html with deploy instead of deleting the site.",
2527
2537
  inputSchema: {
2528
2538
  projectDir: projectDirInput,
2529
2539
  action: z5.enum(["preview", "confirm"]),
@@ -2542,6 +2552,20 @@ function registerLifecycleTools(server, baseCtx) {
2542
2552
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
2543
2553
  operationId
2544
2554
  });
2555
+ if (preview.consequences.requiresFreeModeBeforeDelete) {
2556
+ return structuredToolResult({
2557
+ schemaVersion: 1,
2558
+ outcome: "waiting_user",
2559
+ resultCode: "paid_site_must_return_to_free",
2560
+ operationId,
2561
+ summary: "This paid site cannot be deleted yet. Open portal to cancel renewal, then wait until Stripe ends the subscription and Sakupa reports free mode before calling delete again. If the goal is only a temporary pause, edit the site index.html to show a pause notice and call deploy; this keeps the subscription and URL.",
2562
+ data: { preview },
2563
+ nextActions: [
2564
+ { tool: "portal", allowed: true, reasonCode: "cancel_renewal_first" },
2565
+ { tool: "deploy", allowed: true, reasonCode: "temporary_pause_alternative" }
2566
+ ]
2567
+ });
2568
+ }
2545
2569
  return structuredToolResult({
2546
2570
  schemaVersion: 1,
2547
2571
  outcome: "waiting_user",
@@ -2560,6 +2584,12 @@ function registerLifecycleTools(server, baseCtx) {
2560
2584
  'To CONFIRM deletion, first call delete with action:"preview" to get the exact confirmation object bound to the current site state, then call again with action:"confirm", the same operationId, and that confirmation object.'
2561
2585
  );
2562
2586
  }
2587
+ if (args.confirmation.expectedMode === "paid") {
2588
+ throw new LocalGuidanceError(
2589
+ "state_conflict",
2590
+ "A paid site cannot be deleted. Cancel renewal through portal and wait until it returns to free mode. For a temporary pause, publish a pause notice as index.html."
2591
+ );
2592
+ }
2563
2593
  const result = await ctx.client.deleteSite(site.siteId, site.credential, {
2564
2594
  operationId,
2565
2595
  confirmation: args.confirmation
package/dist/index.js CHANGED
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
124
124
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
125
125
 
126
126
  // ../core/dist/domain/version.js
127
- var SAKUPA_MCP_VERSION = "0.7.19";
127
+ var SAKUPA_MCP_VERSION = "0.7.20";
128
128
 
129
129
  // ../core/dist/domain/errors.js
130
130
  var HTTP_STATUS = {
@@ -450,6 +450,9 @@ var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
450
450
  // ../core/dist/services/subscriptions.js
451
451
  var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
452
452
 
453
+ // ../core/dist/services/lifecycle.js
454
+ var EPHEMERAL_RETENTION_HOURS = 90 * 24;
455
+
453
456
  // src/version.ts
454
457
  var MCP_VERSION = SAKUPA_MCP_VERSION;
455
458
  var CLIENT_TYPE = "sakupa-mcp";
@@ -1560,8 +1563,8 @@ async function diagnoseBinding(input) {
1560
1563
  const apexResolves = apexAnswers.length > 0;
1561
1564
  const allOk = checks.every((c) => c.state === "ok");
1562
1565
  const checklist = checks.map(renderCheck).join("\n") + `
1563
- [${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most DNS panels accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
1564
- const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Sakupa's certificate service validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
1566
+ [${apexResolves ? "OK" : "OPTIONAL"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves; this optional extra entrance is available." : `not configured, which is valid and does NOT block binding. If the user explicitly wants the naked domain too, use the first option their DNS provider supports: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most DNS panels accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) Otherwise leave the apex unconfigured or add a URL redirect from @ to www; never treat this as an error.`);
1567
+ const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Sakupa's certificate service validates and issues within minutes once the required www record above is OK; run bind status again in about 5 minutes to re-check the provider and advance activation)." : "starts after verification.");
1565
1568
  return { checks, apexResolves, allOk, checklist, layers };
1566
1569
  }
1567
1570
  var DNS_RETRY_AFTER_SECONDS = 300;
@@ -1582,7 +1585,7 @@ ${diag.checklist}
1582
1585
 
1583
1586
  ${diag.layers}
1584
1587
 
1585
- ` + (diag.allOk ? "All records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
1588
+ ` + (diag.allOk ? "All required records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
1586
1589
  }
1587
1590
 
1588
1591
  // src/tools/definitions.ts
@@ -2084,7 +2087,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2084
2087
  server.registerTool(
2085
2088
  "bind",
2086
2089
  {
2087
- description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
2090
+ description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. One apex TXT verification covers both, and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
2088
2091
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2089
2092
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
2090
2093
  inputSchema: {
@@ -2102,7 +2105,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2102
2105
  const site = requireSiteFile(ctx);
2103
2106
  if (args.action === "status") {
2104
2107
  const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
2105
- if (res2.status === "verified") {
2108
+ if (res2.bindingStatus === "active") {
2106
2109
  writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
2107
2110
  }
2108
2111
  const apex2 = res2.apexDomain;
@@ -2112,11 +2115,11 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2112
2115
  ...res2.verificationRecord ? { verificationRecord: res2.verificationRecord } : {},
2113
2116
  pendingDnsRecords: res2.pendingDnsRecords,
2114
2117
  verificationStatus: res2.status,
2115
- provisioning: res2.provisioningJobId !== void 0
2118
+ provisioning: res2.bindingStatus === "provisioning"
2116
2119
  });
2117
2120
  const { apexResolves, allOk } = diag;
2118
2121
  return text(
2119
- res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
2122
+ res2.bindingStatus === "active" ? "domain_binding_active" : res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
2120
2123
  `Domain binding status for ${apex2}: ${res2.status}
2121
2124
  ${res2.message}
2122
2125
 
@@ -2127,16 +2130,20 @@ ${res2.message}
2127
2130
  {
2128
2131
  verificationId: res2.verificationId,
2129
2132
  status: res2.status,
2133
+ bindingStatus: res2.bindingStatus,
2130
2134
  apexDomain: apex2,
2131
2135
  provisioningJobId: res2.provisioningJobId,
2132
2136
  servingTarget: res2.servingTarget,
2133
2137
  dnsChecklist: toDnsChecklist(diag.checks),
2134
2138
  apexResolves,
2139
+ apexRequired: false,
2140
+ requiredServingHostname: `www.${apex2}`,
2141
+ optionalApexHostname: apex2,
2135
2142
  retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
2136
2143
  maxAttempts: DNS_MAX_ATTEMPTS,
2137
2144
  message: res2.message
2138
2145
  },
2139
- res2.status === "verified" ? "pending_provider" : "waiting_user"
2146
+ res2.bindingStatus === "active" ? "completed" : res2.status === "verified" ? "pending_provider" : "waiting_user"
2140
2147
  );
2141
2148
  }
2142
2149
  if (!args.hostname) {
@@ -2154,7 +2161,7 @@ ${res2.message}
2154
2161
  ` : "";
2155
2162
  return text(
2156
2163
  "domain_verification_started",
2157
- switchNotice + `Domain binding started for ${apex} (will serve: ${res.includedHostnames.join(", ")}).
2164
+ switchNotice + `Domain binding started for ${apex} (routes reserved: ${res.includedHostnames.join(", ")}). Only www.${apex} is required to go live; the naked domain is optional.
2158
2165
 
2159
2166
  This is a STEP-BY-STEP setup \u2014 give the user ONE record at a time so they do not get overwhelmed and give up.
2160
2167
 
@@ -2169,6 +2176,9 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
2169
2176
  verificationId: res.verificationId,
2170
2177
  apexDomain: apex,
2171
2178
  includedHostnames: res.includedHostnames,
2179
+ apexRequired: false,
2180
+ requiredServingHostname: `www.${apex}`,
2181
+ optionalApexHostname: apex,
2172
2182
  verificationRecord: res.verificationRecord,
2173
2183
  servingTarget: res.servingTarget,
2174
2184
  ...res.supersedes ? { supersedes: res.supersedes } : {},
@@ -2517,7 +2527,7 @@ function registerLifecycleTools(server, baseCtx) {
2517
2527
  server.registerTool(
2518
2528
  "delete",
2519
2529
  {
2520
- description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
2530
+ description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state. Paid sites must first cancel renewal through portal and return to free mode. For a temporary pause, publish a pause notice as index.html with deploy instead of deleting the site.",
2521
2531
  inputSchema: {
2522
2532
  projectDir: projectDirInput,
2523
2533
  action: z4.enum(["preview", "confirm"]),
@@ -2536,6 +2546,20 @@ function registerLifecycleTools(server, baseCtx) {
2536
2546
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
2537
2547
  operationId
2538
2548
  });
2549
+ if (preview.consequences.requiresFreeModeBeforeDelete) {
2550
+ return structuredToolResult({
2551
+ schemaVersion: 1,
2552
+ outcome: "waiting_user",
2553
+ resultCode: "paid_site_must_return_to_free",
2554
+ operationId,
2555
+ summary: "This paid site cannot be deleted yet. Open portal to cancel renewal, then wait until Stripe ends the subscription and Sakupa reports free mode before calling delete again. If the goal is only a temporary pause, edit the site index.html to show a pause notice and call deploy; this keeps the subscription and URL.",
2556
+ data: { preview },
2557
+ nextActions: [
2558
+ { tool: "portal", allowed: true, reasonCode: "cancel_renewal_first" },
2559
+ { tool: "deploy", allowed: true, reasonCode: "temporary_pause_alternative" }
2560
+ ]
2561
+ });
2562
+ }
2539
2563
  return structuredToolResult({
2540
2564
  schemaVersion: 1,
2541
2565
  outcome: "waiting_user",
@@ -2554,6 +2578,12 @@ function registerLifecycleTools(server, baseCtx) {
2554
2578
  'To CONFIRM deletion, first call delete with action:"preview" to get the exact confirmation object bound to the current site state, then call again with action:"confirm", the same operationId, and that confirmation object.'
2555
2579
  );
2556
2580
  }
2581
+ if (args.confirmation.expectedMode === "paid") {
2582
+ throw new LocalGuidanceError(
2583
+ "state_conflict",
2584
+ "A paid site cannot be deleted. Cancel renewal through portal and wait until it returns to free mode. For a temporary pause, publish a pause notice as index.html."
2585
+ );
2586
+ }
2557
2587
  const result = await ctx.client.deleteSite(site.siteId, site.credential, {
2558
2588
  operationId,
2559
2589
  confirmation: args.confirmation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.19",
3
+ "version": "0.7.20",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",