@sakupa/mcp 0.7.16 → 0.7.18

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 +32 -97
  2. package/dist/index.js +32 -97
  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.16";
132
+ var SAKUPA_MCP_VERSION = "0.7.18";
133
133
 
134
134
  // ../core/dist/domain/errors.js
135
135
  var HTTP_STATUS = {
@@ -603,20 +603,6 @@ var HttpApiClient = class {
603
603
  }
604
604
  );
605
605
  }
606
- async unbindDomain(siteId, credential, req) {
607
- return this.call(
608
- "POST",
609
- `/v1/sites/${encodeURIComponent(siteId)}/domain/unbind`,
610
- { credential, body: req }
611
- );
612
- }
613
- async previewUnbindDomain(siteId, credential, req) {
614
- return this.call(
615
- "POST",
616
- `/v1/sites/${encodeURIComponent(siteId)}/domain/unbind/preview`,
617
- { credential, body: req }
618
- );
619
- }
620
606
  async recoverDomain(req) {
621
607
  return this.call("POST", "/v1/domains/recover", { body: req });
622
608
  }
@@ -1497,8 +1483,10 @@ function requireSiteFile(ctx) {
1497
1483
  }
1498
1484
  var UNAUTHORIZED_SUMMARY = "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.";
1499
1485
  function toolError(e) {
1500
- const errorCode = isSakupaError(e) ? e.code : "internal";
1501
- const retryable = errorCode === "rate_limited" || errorCode === "internal";
1486
+ const isSakupa = isSakupaError(e);
1487
+ const errorCode = isSakupa ? e.code : "internal";
1488
+ const retryable = errorCode === "rate_limited" || isSakupa && errorCode === "internal";
1489
+ const opaqueUnclassified = errorCode === "internal" && !retryable;
1502
1490
  const safeDetailKeys = /* @__PURE__ */ new Set([
1503
1491
  "retryAfterSeconds",
1504
1492
  "reasonCode",
@@ -1515,7 +1503,7 @@ function toolError(e) {
1515
1503
  ) : void 0;
1516
1504
  const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
1517
1505
  const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
1518
- const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.");
1506
+ const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying will not help. Tell the user briefly, then offer to file it: run the report tool to send Sakupa a diagnostic report of what was being attempted so we can investigate." : "The operation failed; no server-internal details are exposed.");
1519
1507
  const result = structuredToolResult({
1520
1508
  schemaVersion: 1,
1521
1509
  outcome: "failed",
@@ -2095,9 +2083,12 @@ ${res2.message}
2095
2083
  const res = await ctx.client.bindDomain(site.credential, req);
2096
2084
  const apex = res.apexDomain;
2097
2085
  const txtShort = shortHostFor(res.verificationRecord.name, apex);
2086
+ const switchNotice = res.supersedes ? `NOTE FOR THE USER: this site already serves ${res.supersedes}. ${res.supersedes} keeps working the whole time; once www.${apex} is confirmed live, it automatically REPLACES ${res.supersedes} (zero downtime). Tell the user this before they start.
2087
+
2088
+ ` : "";
2098
2089
  return text(
2099
2090
  "domain_verification_started",
2100
- `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2091
+ switchNotice + `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2101
2092
 
2102
2093
  STEP 1 of 2 \u2014 add ALL THREE records NOW:
2103
2094
 
@@ -2118,6 +2109,7 @@ Drive the whole flow with bind "status": it live-checks every record and names t
2118
2109
  includedHostnames: res.includedHostnames,
2119
2110
  verificationRecord: res.verificationRecord,
2120
2111
  servingTarget: res.servingTarget,
2112
+ ...res.supersedes ? { supersedes: res.supersedes } : {},
2121
2113
  requiredRecords: [
2122
2114
  {
2123
2115
  type: "TXT",
@@ -2517,6 +2509,7 @@ function registerBillingTools(server, baseCtx) {
2517
2509
  }
2518
2510
 
2519
2511
  // src/tools/lifecycle.ts
2512
+ import { randomUUID as randomUUID2 } from "node:crypto";
2520
2513
  import { z as z5 } from "zod";
2521
2514
  var deleteConfirmation = z5.object({
2522
2515
  siteId: z5.string().min(1),
@@ -2533,15 +2526,6 @@ var deleteConfirmation = z5.object({
2533
2526
  expectedBoundHostnames: z5.array(z5.string()),
2534
2527
  acknowledge: z5.literal("delete_and_cancel_renewal")
2535
2528
  });
2536
- var unbindConfirmation = z5.object({
2537
- siteId: z5.string().min(1),
2538
- bindingId: z5.string().min(1),
2539
- expectedBindingUpdatedAt: z5.string().datetime(),
2540
- expectedBindingStatus: z5.enum(["provisioning", "active"]),
2541
- apexDomain: z5.string().min(1),
2542
- expectedBoundHostnames: z5.array(z5.string()),
2543
- acknowledge: z5.literal("unbind_and_remove_custom_hostnames")
2544
- });
2545
2529
  function registerLifecycleTools(server, baseCtx) {
2546
2530
  server.registerTool(
2547
2531
  "delete",
@@ -2560,28 +2544,31 @@ function registerLifecycleTools(server, baseCtx) {
2560
2544
  try {
2561
2545
  const ctx = withProjectDir(baseCtx, args.projectDir);
2562
2546
  const site = requireSiteFile(ctx);
2563
- if (!args.operationId) {
2564
- throw new Error("operationId is required for delete");
2565
- }
2547
+ const operationId = args.operationId ?? randomUUID2();
2566
2548
  if (args.action === "preview") {
2567
2549
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
2568
- operationId: args.operationId
2550
+ operationId
2569
2551
  });
2570
2552
  return structuredToolResult({
2571
2553
  schemaVersion: 1,
2572
2554
  outcome: "waiting_user",
2573
2555
  resultCode: "delete_confirmation_required",
2574
- operationId: args.operationId,
2575
- summary: "Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable.",
2556
+ operationId,
2557
+ summary: `Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable. To proceed, call delete again with action:"confirm", operationId:"${operationId}", and the confirmation object from data.preview.confirmation.`,
2576
2558
  data: { preview },
2577
2559
  nextActions: [
2578
2560
  { tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
2579
2561
  ]
2580
2562
  });
2581
2563
  }
2582
- if (!args.confirmation) throw new Error("confirmation is required for confirm");
2564
+ if (!args.confirmation) {
2565
+ throw new LocalGuidanceError(
2566
+ "invalid_request",
2567
+ '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
+ );
2569
+ }
2583
2570
  const result = await ctx.client.deleteSite(site.siteId, site.credential, {
2584
- operationId: args.operationId,
2571
+ operationId,
2585
2572
  confirmation: args.confirmation
2586
2573
  });
2587
2574
  deleteSiteFile(ctx.projectDir);
@@ -2590,7 +2577,7 @@ function registerLifecycleTools(server, baseCtx) {
2590
2577
  schemaVersion: 1,
2591
2578
  outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2592
2579
  resultCode: "site_deleted",
2593
- operationId: args.operationId,
2580
+ operationId,
2594
2581
  summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
2595
2582
  data: { result, projectDir: ctx.projectDir },
2596
2583
  nextActions: []
@@ -2600,61 +2587,6 @@ function registerLifecycleTools(server, baseCtx) {
2600
2587
  }
2601
2588
  }
2602
2589
  );
2603
- server.registerTool(
2604
- "unbind",
2605
- {
2606
- description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
2607
- inputSchema: {
2608
- projectDir: projectDirInput,
2609
- action: z5.enum(["preview", "confirm"]),
2610
- operationId: z5.string().min(1).optional(),
2611
- confirmation: unbindConfirmation.optional()
2612
- },
2613
- outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2614
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
2615
- },
2616
- async (args) => {
2617
- try {
2618
- const ctx = withProjectDir(baseCtx, args.projectDir);
2619
- const site = requireSiteFile(ctx);
2620
- if (!args.operationId) throw new Error("operationId is required for unbind");
2621
- if (args.action === "preview") {
2622
- const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
2623
- operationId: args.operationId
2624
- });
2625
- return structuredToolResult({
2626
- schemaVersion: 1,
2627
- outcome: "waiting_user",
2628
- resultCode: "unbind_confirmation_required",
2629
- operationId: args.operationId,
2630
- summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
2631
- data: { preview },
2632
- nextActions: [
2633
- { tool: "unbind", allowed: true, reasonCode: "exact_confirmation_required" }
2634
- ]
2635
- });
2636
- }
2637
- if (!args.confirmation) throw new Error("confirmation is required for confirm");
2638
- const result = await ctx.client.unbindDomain(site.siteId, site.credential, {
2639
- operationId: args.operationId,
2640
- confirmation: args.confirmation
2641
- });
2642
- const { boundDomain: _removed, ...remaining } = site;
2643
- writeSiteFile(ctx.projectDir, remaining);
2644
- return structuredToolResult({
2645
- schemaVersion: 1,
2646
- outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2647
- resultCode: "domain_unbound",
2648
- operationId: args.operationId,
2649
- summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
2650
- data: { result },
2651
- nextActions: [{ tool: "status", allowed: true }]
2652
- });
2653
- } catch (error) {
2654
- return toolError(error);
2655
- }
2656
- }
2657
- );
2658
2590
  }
2659
2591
 
2660
2592
  // src/transport.ts
@@ -2722,8 +2654,10 @@ var FetchTransport = class {
2722
2654
  });
2723
2655
  if (!res.ok) {
2724
2656
  const text2 = await res.text().catch(() => "");
2725
- throw new Error(
2726
- `Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`
2657
+ const detail = `Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`;
2658
+ throw new SakupaError(
2659
+ res.status === 429 || res.status >= 500 ? "internal" : "validation_failed",
2660
+ detail
2727
2661
  );
2728
2662
  }
2729
2663
  }
@@ -2751,8 +2685,9 @@ Workflow:
2751
2685
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
2752
2686
  first verified request wins; unverified requests expire after 72 hours. billing,
2753
2687
  upgrade, portal and recover manage the paid
2754
- lifecycle; unbind removes the custom domain (the permanent URL keeps serving) and
2755
- delete tears the whole site down after explicit confirmation.
2688
+ lifecycle; delete tears the whole site down after explicit confirmation. Binding a
2689
+ NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
2690
+ until the new domain's www is confirmed live, then it is replaced automatically.
2756
2691
  Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2757
2692
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
2758
2693
  24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
@@ -2766,7 +2701,7 @@ the user opened (for framework projects, where package.json lives \u2014 never t
2766
2701
  build folder; output is auto-detected). The server NEVER guesses a directory and refuses
2767
2702
  calls without one: only you can see which directory the user is actually in. After every deploy, TELL the user which environment it went to (deploy results carry an
2768
2703
  Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
2769
- refresh, delete and unbind echo
2704
+ refresh and delete echo
2770
2705
  the directory they acted on \u2014 verify it matches the user's active project.
2771
2706
 
2772
2707
  When the same operation fails twice in a row, or the user is clearly stuck or
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.16";
127
+ var SAKUPA_MCP_VERSION = "0.7.18";
128
128
 
129
129
  // ../core/dist/domain/errors.js
130
130
  var HTTP_STATUS = {
@@ -546,8 +546,10 @@ var FetchTransport = class {
546
546
  });
547
547
  if (!res.ok) {
548
548
  const text2 = await res.text().catch(() => "");
549
- throw new Error(
550
- `Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`
549
+ const detail = `Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`;
550
+ throw new SakupaError(
551
+ res.status === 429 || res.status >= 500 ? "internal" : "validation_failed",
552
+ detail
551
553
  );
552
554
  }
553
555
  }
@@ -671,20 +673,6 @@ var HttpApiClient = class {
671
673
  }
672
674
  );
673
675
  }
674
- async unbindDomain(siteId, credential, req) {
675
- return this.call(
676
- "POST",
677
- `/v1/sites/${encodeURIComponent(siteId)}/domain/unbind`,
678
- { credential, body: req }
679
- );
680
- }
681
- async previewUnbindDomain(siteId, credential, req) {
682
- return this.call(
683
- "POST",
684
- `/v1/sites/${encodeURIComponent(siteId)}/domain/unbind/preview`,
685
- { credential, body: req }
686
- );
687
- }
688
676
  async recoverDomain(req) {
689
677
  return this.call("POST", "/v1/domains/recover", { body: req });
690
678
  }
@@ -1378,8 +1366,10 @@ function requireSiteFile(ctx) {
1378
1366
  }
1379
1367
  var UNAUTHORIZED_SUMMARY = "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.";
1380
1368
  function toolError(e) {
1381
- const errorCode = isSakupaError(e) ? e.code : "internal";
1382
- const retryable = errorCode === "rate_limited" || errorCode === "internal";
1369
+ const isSakupa = isSakupaError(e);
1370
+ const errorCode = isSakupa ? e.code : "internal";
1371
+ const retryable = errorCode === "rate_limited" || isSakupa && errorCode === "internal";
1372
+ const opaqueUnclassified = errorCode === "internal" && !retryable;
1383
1373
  const safeDetailKeys = /* @__PURE__ */ new Set([
1384
1374
  "retryAfterSeconds",
1385
1375
  "reasonCode",
@@ -1396,7 +1386,7 @@ function toolError(e) {
1396
1386
  ) : void 0;
1397
1387
  const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
1398
1388
  const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
1399
- const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.");
1389
+ const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying will not help. Tell the user briefly, then offer to file it: run the report tool to send Sakupa a diagnostic report of what was being attempted so we can investigate." : "The operation failed; no server-internal details are exposed.");
1400
1390
  const result = structuredToolResult({
1401
1391
  schemaVersion: 1,
1402
1392
  outcome: "failed",
@@ -2159,9 +2149,12 @@ ${res2.message}
2159
2149
  const res = await ctx.client.bindDomain(site.credential, req);
2160
2150
  const apex = res.apexDomain;
2161
2151
  const txtShort = shortHostFor(res.verificationRecord.name, apex);
2152
+ const switchNotice = res.supersedes ? `NOTE FOR THE USER: this site already serves ${res.supersedes}. ${res.supersedes} keeps working the whole time; once www.${apex} is confirmed live, it automatically REPLACES ${res.supersedes} (zero downtime). Tell the user this before they start.
2153
+
2154
+ ` : "";
2162
2155
  return text(
2163
2156
  "domain_verification_started",
2164
- `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2157
+ switchNotice + `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2165
2158
 
2166
2159
  STEP 1 of 2 \u2014 add ALL THREE records NOW:
2167
2160
 
@@ -2182,6 +2175,7 @@ Drive the whole flow with bind "status": it live-checks every record and names t
2182
2175
  includedHostnames: res.includedHostnames,
2183
2176
  verificationRecord: res.verificationRecord,
2184
2177
  servingTarget: res.servingTarget,
2178
+ ...res.supersedes ? { supersedes: res.supersedes } : {},
2185
2179
  requiredRecords: [
2186
2180
  {
2187
2181
  type: "TXT",
@@ -2509,6 +2503,7 @@ Summary: ${res.sanitizedSummary}`,
2509
2503
  }
2510
2504
 
2511
2505
  // src/tools/lifecycle.ts
2506
+ import { randomUUID as randomUUID2 } from "node:crypto";
2512
2507
  import { z as z4 } from "zod";
2513
2508
  var deleteConfirmation = z4.object({
2514
2509
  siteId: z4.string().min(1),
@@ -2525,15 +2520,6 @@ var deleteConfirmation = z4.object({
2525
2520
  expectedBoundHostnames: z4.array(z4.string()),
2526
2521
  acknowledge: z4.literal("delete_and_cancel_renewal")
2527
2522
  });
2528
- var unbindConfirmation = z4.object({
2529
- siteId: z4.string().min(1),
2530
- bindingId: z4.string().min(1),
2531
- expectedBindingUpdatedAt: z4.string().datetime(),
2532
- expectedBindingStatus: z4.enum(["provisioning", "active"]),
2533
- apexDomain: z4.string().min(1),
2534
- expectedBoundHostnames: z4.array(z4.string()),
2535
- acknowledge: z4.literal("unbind_and_remove_custom_hostnames")
2536
- });
2537
2523
  function registerLifecycleTools(server, baseCtx) {
2538
2524
  server.registerTool(
2539
2525
  "delete",
@@ -2552,28 +2538,31 @@ function registerLifecycleTools(server, baseCtx) {
2552
2538
  try {
2553
2539
  const ctx = withProjectDir(baseCtx, args.projectDir);
2554
2540
  const site = requireSiteFile(ctx);
2555
- if (!args.operationId) {
2556
- throw new Error("operationId is required for delete");
2557
- }
2541
+ const operationId = args.operationId ?? randomUUID2();
2558
2542
  if (args.action === "preview") {
2559
2543
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
2560
- operationId: args.operationId
2544
+ operationId
2561
2545
  });
2562
2546
  return structuredToolResult({
2563
2547
  schemaVersion: 1,
2564
2548
  outcome: "waiting_user",
2565
2549
  resultCode: "delete_confirmation_required",
2566
- operationId: args.operationId,
2567
- summary: "Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable.",
2550
+ operationId,
2551
+ summary: `Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable. To proceed, call delete again with action:"confirm", operationId:"${operationId}", and the confirmation object from data.preview.confirmation.`,
2568
2552
  data: { preview },
2569
2553
  nextActions: [
2570
2554
  { tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
2571
2555
  ]
2572
2556
  });
2573
2557
  }
2574
- if (!args.confirmation) throw new Error("confirmation is required for confirm");
2558
+ if (!args.confirmation) {
2559
+ throw new LocalGuidanceError(
2560
+ "invalid_request",
2561
+ '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
+ );
2563
+ }
2575
2564
  const result = await ctx.client.deleteSite(site.siteId, site.credential, {
2576
- operationId: args.operationId,
2565
+ operationId,
2577
2566
  confirmation: args.confirmation
2578
2567
  });
2579
2568
  deleteSiteFile(ctx.projectDir);
@@ -2582,7 +2571,7 @@ function registerLifecycleTools(server, baseCtx) {
2582
2571
  schemaVersion: 1,
2583
2572
  outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2584
2573
  resultCode: "site_deleted",
2585
- operationId: args.operationId,
2574
+ operationId,
2586
2575
  summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
2587
2576
  data: { result, projectDir: ctx.projectDir },
2588
2577
  nextActions: []
@@ -2592,61 +2581,6 @@ function registerLifecycleTools(server, baseCtx) {
2592
2581
  }
2593
2582
  }
2594
2583
  );
2595
- server.registerTool(
2596
- "unbind",
2597
- {
2598
- description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
2599
- inputSchema: {
2600
- projectDir: projectDirInput,
2601
- action: z4.enum(["preview", "confirm"]),
2602
- operationId: z4.string().min(1).optional(),
2603
- confirmation: unbindConfirmation.optional()
2604
- },
2605
- outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2606
- annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
2607
- },
2608
- async (args) => {
2609
- try {
2610
- const ctx = withProjectDir(baseCtx, args.projectDir);
2611
- const site = requireSiteFile(ctx);
2612
- if (!args.operationId) throw new Error("operationId is required for unbind");
2613
- if (args.action === "preview") {
2614
- const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
2615
- operationId: args.operationId
2616
- });
2617
- return structuredToolResult({
2618
- schemaVersion: 1,
2619
- outcome: "waiting_user",
2620
- resultCode: "unbind_confirmation_required",
2621
- operationId: args.operationId,
2622
- summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
2623
- data: { preview },
2624
- nextActions: [
2625
- { tool: "unbind", allowed: true, reasonCode: "exact_confirmation_required" }
2626
- ]
2627
- });
2628
- }
2629
- if (!args.confirmation) throw new Error("confirmation is required for confirm");
2630
- const result = await ctx.client.unbindDomain(site.siteId, site.credential, {
2631
- operationId: args.operationId,
2632
- confirmation: args.confirmation
2633
- });
2634
- const { boundDomain: _removed, ...remaining } = site;
2635
- writeSiteFile(ctx.projectDir, remaining);
2636
- return structuredToolResult({
2637
- schemaVersion: 1,
2638
- outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2639
- resultCode: "domain_unbound",
2640
- operationId: args.operationId,
2641
- summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
2642
- data: { result },
2643
- nextActions: [{ tool: "status", allowed: true }]
2644
- });
2645
- } catch (error) {
2646
- return toolError(error);
2647
- }
2648
- }
2649
- );
2650
2584
  }
2651
2585
 
2652
2586
  // src/server.ts
@@ -2746,8 +2680,9 @@ Workflow:
2746
2680
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
2747
2681
  first verified request wins; unverified requests expire after 72 hours. billing,
2748
2682
  upgrade, portal and recover manage the paid
2749
- lifecycle; unbind removes the custom domain (the permanent URL keeps serving) and
2750
- delete tears the whole site down after explicit confirmation.
2683
+ lifecycle; delete tears the whole site down after explicit confirmation. Binding a
2684
+ NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
2685
+ until the new domain's www is confirmed live, then it is replaced automatically.
2751
2686
  Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2752
2687
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
2753
2688
  24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
@@ -2761,7 +2696,7 @@ the user opened (for framework projects, where package.json lives \u2014 never t
2761
2696
  build folder; output is auto-detected). The server NEVER guesses a directory and refuses
2762
2697
  calls without one: only you can see which directory the user is actually in. After every deploy, TELL the user which environment it went to (deploy results carry an
2763
2698
  Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
2764
- refresh, delete and unbind echo
2699
+ refresh and delete echo
2765
2700
  the directory they acted on \u2014 verify it matches the user's active project.
2766
2701
 
2767
2702
  When the same operation fails twice in a row, or the user is clearly stuck or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
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",