@sakupa/mcp 0.7.18 → 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 +47 -24
  2. package/dist/index.js +47 -24
  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.18";
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,25 +2095,24 @@ ${res2.message}
2088
2095
  ` : "";
2089
2096
  return text(
2090
2097
  "domain_verification_started",
2091
- switchNotice + `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2092
-
2093
- STEP 1 of 2 \u2014 add ALL THREE records NOW:
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.
2094
2099
 
2095
- 1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
2096
- 2) CNAME host: www value: ${res.servingTarget}
2097
- 3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
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.
2098
2101
 
2099
- Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
2102
+ STEP 1 of 2 \u2014 prove ownership. Add ONE record:
2100
2103
 
2101
- Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2104
+ TXT host: ${txtShort} value: ${res.verificationRecord.value}
2102
2105
 
2103
- STEP 2 (after ownership verifies): Sakupa issues certificate-validation TXT records \u2014 the verification result and bind "status" list them the moment they exist; relay each to the user and add them too (copy names and values EXACTLY as given). Everything then completes automatically.
2106
+ Host is the SHORT form: most panels append the domain automatically (the saved record must NOT show ${apex} twice in one name). Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2104
2107
 
2105
- Drive the whole flow with bind "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
2108
+ When the user says the TXT is set, run bind "status". It verifies ownership and then hands back STEP 2 \u2014 a SINGLE www CNAME (there are NO certificate TXT records; HTTPS validates automatically over that CNAME). Each "status" checks the previous step and, unless something is misconfigured, advances to the next \u2014 so run it whenever the user reports a step done, NOT on a timer. Any later session can resume with action "status" alone; the verificationId is optional.`,
2106
2109
  {
2107
2110
  verificationId: res.verificationId,
2108
2111
  apexDomain: apex,
2109
2112
  includedHostnames: res.includedHostnames,
2113
+ apexRequired: false,
2114
+ requiredServingHostname: `www.${apex}`,
2115
+ optionalApexHostname: apex,
2110
2116
  verificationRecord: res.verificationRecord,
2111
2117
  servingTarget: res.servingTarget,
2112
2118
  ...res.supersedes ? { supersedes: res.supersedes } : {},
@@ -2116,11 +2122,8 @@ Drive the whole flow with bind "status": it live-checks every record and names t
2116
2122
  shortHost: txtShort,
2117
2123
  name: res.verificationRecord.name,
2118
2124
  value: res.verificationRecord.value
2119
- },
2120
- { type: "CNAME", shortHost: "www", name: `www.${apex}`, value: res.servingTarget },
2121
- { type: "ALIAS", shortHost: "@", name: apex, value: res.servingTarget }
2125
+ }
2122
2126
  ],
2123
- servingInstructions: res.servingInstructions,
2124
2127
  retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
2125
2128
  maxAttempts: DNS_MAX_ATTEMPTS
2126
2129
  },
@@ -2530,7 +2533,7 @@ function registerLifecycleTools(server, baseCtx) {
2530
2533
  server.registerTool(
2531
2534
  "delete",
2532
2535
  {
2533
- 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.",
2534
2537
  inputSchema: {
2535
2538
  projectDir: projectDirInput,
2536
2539
  action: z5.enum(["preview", "confirm"]),
@@ -2549,6 +2552,20 @@ function registerLifecycleTools(server, baseCtx) {
2549
2552
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
2550
2553
  operationId
2551
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
+ }
2552
2569
  return structuredToolResult({
2553
2570
  schemaVersion: 1,
2554
2571
  outcome: "waiting_user",
@@ -2567,6 +2584,12 @@ function registerLifecycleTools(server, baseCtx) {
2567
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.'
2568
2585
  );
2569
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
+ }
2570
2593
  const result = await ctx.client.deleteSite(site.siteId, site.credential, {
2571
2594
  operationId,
2572
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.18";
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,25 +2161,24 @@ ${res2.message}
2154
2161
  ` : "";
2155
2162
  return text(
2156
2163
  "domain_verification_started",
2157
- switchNotice + `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2158
-
2159
- STEP 1 of 2 \u2014 add ALL THREE records NOW:
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.
2160
2165
 
2161
- 1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
2162
- 2) CNAME host: www value: ${res.servingTarget}
2163
- 3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
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.
2164
2167
 
2165
- Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
2168
+ STEP 1 of 2 \u2014 prove ownership. Add ONE record:
2166
2169
 
2167
- Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2170
+ TXT host: ${txtShort} value: ${res.verificationRecord.value}
2168
2171
 
2169
- STEP 2 (after ownership verifies): Sakupa issues certificate-validation TXT records \u2014 the verification result and bind "status" list them the moment they exist; relay each to the user and add them too (copy names and values EXACTLY as given). Everything then completes automatically.
2172
+ Host is the SHORT form: most panels append the domain automatically (the saved record must NOT show ${apex} twice in one name). Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2170
2173
 
2171
- Drive the whole flow with bind "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
2174
+ When the user says the TXT is set, run bind "status". It verifies ownership and then hands back STEP 2 \u2014 a SINGLE www CNAME (there are NO certificate TXT records; HTTPS validates automatically over that CNAME). Each "status" checks the previous step and, unless something is misconfigured, advances to the next \u2014 so run it whenever the user reports a step done, NOT on a timer. Any later session can resume with action "status" alone; the verificationId is optional.`,
2172
2175
  {
2173
2176
  verificationId: res.verificationId,
2174
2177
  apexDomain: apex,
2175
2178
  includedHostnames: res.includedHostnames,
2179
+ apexRequired: false,
2180
+ requiredServingHostname: `www.${apex}`,
2181
+ optionalApexHostname: apex,
2176
2182
  verificationRecord: res.verificationRecord,
2177
2183
  servingTarget: res.servingTarget,
2178
2184
  ...res.supersedes ? { supersedes: res.supersedes } : {},
@@ -2182,11 +2188,8 @@ Drive the whole flow with bind "status": it live-checks every record and names t
2182
2188
  shortHost: txtShort,
2183
2189
  name: res.verificationRecord.name,
2184
2190
  value: res.verificationRecord.value
2185
- },
2186
- { type: "CNAME", shortHost: "www", name: `www.${apex}`, value: res.servingTarget },
2187
- { type: "ALIAS", shortHost: "@", name: apex, value: res.servingTarget }
2191
+ }
2188
2192
  ],
2189
- servingInstructions: res.servingInstructions,
2190
2193
  retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
2191
2194
  maxAttempts: DNS_MAX_ATTEMPTS
2192
2195
  },
@@ -2524,7 +2527,7 @@ function registerLifecycleTools(server, baseCtx) {
2524
2527
  server.registerTool(
2525
2528
  "delete",
2526
2529
  {
2527
- 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.",
2528
2531
  inputSchema: {
2529
2532
  projectDir: projectDirInput,
2530
2533
  action: z4.enum(["preview", "confirm"]),
@@ -2543,6 +2546,20 @@ function registerLifecycleTools(server, baseCtx) {
2543
2546
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
2544
2547
  operationId
2545
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
+ }
2546
2563
  return structuredToolResult({
2547
2564
  schemaVersion: 1,
2548
2565
  outcome: "waiting_user",
@@ -2561,6 +2578,12 @@ function registerLifecycleTools(server, baseCtx) {
2561
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.'
2562
2579
  );
2563
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
+ }
2564
2587
  const result = await ctx.client.deleteSite(site.siteId, site.credential, {
2565
2588
  operationId,
2566
2589
  confirmation: args.confirmation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.18",
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",