@sakupa/mcp 1.4.0 → 1.5.0

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 +238 -24
  2. package/dist/index.js +238 -24
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -259,7 +259,8 @@ var DEFAULT_API_BASE_URL = "https://api.sakupa.com";
259
259
  var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
260
260
  var TEST_ACCESS_HEADER = "x-sakupa-test-token";
261
261
  var CREDENTIAL_ROTATION_RECOMMEND_AFTER_SECONDS = 7 * 24 * 60 * 60;
262
- var FREE_SITE_TTL_HOURS = 24;
262
+ var FREE_SITE_TTL_DAYS = 30;
263
+ var FREE_SITE_TTL_HOURS = FREE_SITE_TTL_DAYS * 24;
263
264
  var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
264
265
  var FREE_ACTIVE_SITES_PER_IP = 3;
265
266
  var PAID_SITE_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
@@ -402,7 +403,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
402
403
  }
403
404
 
404
405
  // ../core/dist/domain/version.js
405
- var SAKUPA_MCP_VERSION = "1.4.0";
406
+ var SAKUPA_MCP_VERSION = "1.5.0";
406
407
 
407
408
  // ../core/dist/domain/errors.js
408
409
  var HTTP_STATUS = {
@@ -2207,15 +2208,15 @@ function strFromU8(dat, latin1) {
2207
2208
  var slzh = function(d, b) {
2208
2209
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
2209
2210
  };
2210
- var zh = function(d, b, z7) {
2211
+ var zh = function(d, b, z8) {
2211
2212
  var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
2212
- var _a2 = z64hs(d, es, efl, z7, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
2213
+ var _a2 = z64hs(d, es, efl, z8, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
2213
2214
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
2214
2215
  };
2215
- var z64hs = function(d, b, l, z7, sc, su, off) {
2216
+ var z64hs = function(d, b, l, z8, sc, su, off) {
2216
2217
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
2217
2218
  var nf = nsc + nsu + noff;
2218
- if (z7 && nf) {
2219
+ if (z8 && nf) {
2219
2220
  for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
2220
2221
  if (b2(d, b) == 1) {
2221
2222
  return [
@@ -2226,7 +2227,7 @@ var z64hs = function(d, b, l, z7, sc, su, off) {
2226
2227
  ];
2227
2228
  }
2228
2229
  }
2229
- if (z7 < 2)
2230
+ if (z8 < 2)
2230
2231
  err(13);
2231
2232
  }
2232
2233
  return [sc, su, off, 0];
@@ -2243,18 +2244,18 @@ function unzipSync(data, opts) {
2243
2244
  if (!c)
2244
2245
  return {};
2245
2246
  var o = b4(data, e + 16);
2246
- var z7 = b4(data, e - 20) == 117853008;
2247
- if (z7) {
2247
+ var z8 = b4(data, e - 20) == 117853008;
2248
+ if (z8) {
2248
2249
  var ze = b4(data, e - 12);
2249
- z7 = b4(data, ze) == 101075792;
2250
- if (z7) {
2250
+ z8 = b4(data, ze) == 101075792;
2251
+ if (z8) {
2251
2252
  c = b4(data, ze + 32);
2252
2253
  o = b4(data, ze + 48);
2253
2254
  }
2254
2255
  }
2255
2256
  var fltr = opts && opts.filter;
2256
2257
  for (var i = 0; i < c; ++i) {
2257
- var _a2 = zh(data, o, z7), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
2258
+ var _a2 = zh(data, o, z8), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
2258
2259
  o = no;
2259
2260
  if (!fltr || fltr({
2260
2261
  name: fn,
@@ -3435,7 +3436,8 @@ var TARGET_MCP_TOOL_NAMES = [
3435
3436
  "change",
3436
3437
  "support",
3437
3438
  "report",
3438
- "apps"
3439
+ "apps",
3440
+ "delete"
3439
3441
  ];
3440
3442
  var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
3441
3443
  schemaVersion: z.literal(1),
@@ -4419,7 +4421,7 @@ function registerTools(server, baseCtx) {
4419
4421
  "deploy",
4420
4422
  {
4421
4423
  title: "Deploy site",
4422
- description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4424
+ description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_DAYS} days, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4423
4425
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4424
4426
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4425
4427
  inputSchema: z2.object({
@@ -4436,7 +4438,7 @@ function registerTools(server, baseCtx) {
4436
4438
  "Override automatic SPA-fallback detection (single index.html + JS auto-enables rewriting unknown paths to index.html; multiple HTML pages auto-disable it). Pass only to force the behavior against the detected structure."
4437
4439
  ),
4438
4440
  publicConfirmed: z2.boolean().optional().describe(
4439
- "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
4441
+ "Required only for the first deployment: user explicitly confirmed creation of a public URL valid for the free-site window."
4440
4442
  ),
4441
4443
  reuseSiteUrl: z2.string().url().optional().describe(
4442
4444
  "Exact existing free-site URL selected by the user when the three-site free-site allowance is full. Never invent this value; copy it from deploy nextActions."
@@ -4708,9 +4710,10 @@ function registerTools(server, baseCtx) {
4708
4710
  const confirmArguments = { ...args, ...confirmation };
4709
4711
  return presentDecision(baseCtx.decisions, call, "deploy", {
4710
4712
  resultCode: "public_deployment_confirmation_required",
4711
- summary: `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Nothing has been uploaded or made public yet. The exact confirmation field is publicConfirmed: true.`,
4713
+ summary: `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_DAYS} days. Nothing has been uploaded or made public yet. The exact confirmation field is publicConfirmed: true.`,
4712
4714
  data: {
4713
4715
  publicUrlLifetimeHours: FREE_SITE_TTL_HOURS,
4716
+ publicUrlLifetimeDays: FREE_SITE_TTL_DAYS,
4714
4717
  confirmationField: "publicConfirmed",
4715
4718
  confirmation,
4716
4719
  confirmArguments
@@ -4720,7 +4723,7 @@ function registerTools(server, baseCtx) {
4720
4723
  callToolDecisionOption({
4721
4724
  id: "create_public_preview",
4722
4725
  label: "Create the public preview",
4723
- description: `Publish the selected files at a public URL for ${FREE_SITE_TTL_HOURS} hours.`,
4726
+ description: `Publish the selected files at a public URL for ${FREE_SITE_TTL_DAYS} days.`,
4724
4727
  consequences: ["Anyone with the generated URL can open the site."],
4725
4728
  tool: "deploy",
4726
4729
  arguments: confirmArguments,
@@ -4932,7 +4935,7 @@ function registerTools(server, baseCtx) {
4932
4935
  ["Credential path", ".sakupa/site.json"]
4933
4936
  ],
4934
4937
  notes: [
4935
- `This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; while a subscription remains active, this URL stays live without the free-site expiry. This is conditional on the subscription remaining active: do NOT describe the site as permanent or long-term, and do NOT say the subscription is bound to the site.`,
4938
+ `This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_DAYS} days. Deploying again or calling refresh extends the validity; while a subscription remains active, this URL stays live without the free-site expiry. This is conditional on the subscription remaining active: do NOT describe the site as permanent or long-term, and do NOT say the subscription is bound to the site.`,
4936
4939
  "The management credential was saved to the exact relative path .sakupa/site.json \u2014 preserve this complete path verbatim and never shorten it to site.json. Keep that file: it is the only way to manage this site.",
4937
4940
  ...[credentialGitReminder(ctx.projectDir)].filter((line) => line.trim().length > 0)
4938
4941
  ],
@@ -5052,7 +5055,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}
5052
5055
  ...credentialRotationResumed ? [
5053
5056
  "A previously confirmed credential rotation was resumed safely before this deploy; every older credential is revoked."
5054
5057
  ] : [],
5055
- finalized.mode === "free" ? `Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. While a subscription remains active, the site stays live without this free-site expiry.` : "This site is subscription-backed and has no free-site expiry while the subscription remains active.",
5058
+ finalized.mode === "free" ? `Reminder: free sites stay live for ${FREE_SITE_TTL_DAYS} days after the last deploy or refresh call. While a subscription remains active, the site stays live without this free-site expiry.` : "This site is subscription-backed and has no free-site expiry while the subscription remains active.",
5056
5059
  ...credentialSecurity?.rotationRecommended ? [
5057
5060
  `Optional security recommendation: this management credential was created at ${timestampForAgent(credentialSecurity.credentialCreatedAt)} and is older than 7 days. The deploy SUCCEEDED and rotation is not required. Ask the user whether they want to rotate; call rotate without confirmed:true to show the exact revocation preview. Never rotate automatically.`
5058
5061
  ] : []
@@ -5154,7 +5157,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}
5154
5157
  ],
5155
5158
  notes: [
5156
5159
  "NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy.",
5157
- `Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`
5160
+ `Free sites stay live for ${FREE_SITE_TTL_DAYS} days after each deploy or refresh.`
5158
5161
  ],
5159
5162
  next: ["`status`", "`deploy` to publish changed files"]
5160
5163
  }),
@@ -5206,7 +5209,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}
5206
5209
  "subscribe",
5207
5210
  {
5208
5211
  title: "Subscribe (Stripe Checkout)",
5209
- description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 24-hour expiry. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
5212
+ description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 30-day expiry. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
5210
5213
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5211
5214
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5212
5215
  inputSchema: z2.object({
@@ -6168,6 +6171,7 @@ var TOOL_TOPICS = [
6168
6171
  "support",
6169
6172
  "report",
6170
6173
  "apps",
6174
+ "delete",
6171
6175
  "help"
6172
6176
  ];
6173
6177
  var HELP_TOPICS = ["diagnose", "overview", "terminology", ...TOOL_TOPICS];
@@ -6278,6 +6282,18 @@ var TOOL_MANUALS = {
6278
6282
  nextStep: "Use the preview resumeWith arguments only after the user confirms.",
6279
6283
  terminology: ["credentialRotation"]
6280
6284
  },
6285
+ delete: {
6286
+ purpose: "Delete the bound FREE site after an explicit preview and confirmation.",
6287
+ sideEffects: "Preview is read-only. Confirmed deletion removes content, the public URL, installed apps, stored submissions and the local .sakupa/site.json.",
6288
+ preconditions: "A valid local site credential and a free site. A subscribed site is refused until the subscription ends (change \u2192 period-end cancellation) and the site reverts to free.",
6289
+ parameterNames: ["confirmed", "operationId", "confirmation"],
6290
+ parameters: "No arguments for the preview; confirmed=true with the exact operationId and confirmation copied from the preview decision.",
6291
+ warnings: [
6292
+ "Irreversible; the next deploy creates a brand-new site with a new URL.",
6293
+ "Never propose delete to free up the free-site allowance; use the deploy handoff decision instead."
6294
+ ],
6295
+ nextStep: "Present every decision option; after confirmation copy the option arguments verbatim."
6296
+ },
6281
6297
  plans: {
6282
6298
  purpose: "Read the authoritative hosting plan catalog and rules.",
6283
6299
  sideEffects: "Read-only public API request.",
@@ -7256,6 +7272,196 @@ function registerAppsTools(server, baseCtx) {
7256
7272
  );
7257
7273
  }
7258
7274
 
7275
+ // src/tools/delete.ts
7276
+ import { randomUUID as randomUUID7 } from "node:crypto";
7277
+ import { z as z7 } from "zod";
7278
+ var confirmationSchema = z7.object({
7279
+ siteId: z7.string(),
7280
+ expectedSiteUpdatedAt: z7.string(),
7281
+ expectedStatus: z7.string(),
7282
+ expectedMode: z7.enum(["free", "paid"]),
7283
+ expectedServingMode: z7.string(),
7284
+ expectedShortId: z7.string().optional(),
7285
+ expectedSubscriptionStatus: z7.string().optional(),
7286
+ expectedPlan: z7.string().optional(),
7287
+ expectedCancelAtPeriodEnd: z7.boolean().optional(),
7288
+ expectedCurrentPeriodEnd: z7.string().optional(),
7289
+ expectedLastDeploymentId: z7.string().optional(),
7290
+ expectedBoundHostnames: z7.array(z7.string()),
7291
+ acknowledge: z7.literal("delete_and_cancel_renewal")
7292
+ });
7293
+ function registerDeleteTools(server, baseCtx) {
7294
+ server.registerTool(
7295
+ "delete",
7296
+ {
7297
+ title: "Delete site",
7298
+ description: "Delete the site bound to this project. Without arguments it only previews (zero writes) and returns a decision; after the user confirms, call again with the exact preview arguments (confirmed: true, operationId, confirmation). A FREE site is deleted immediately: content, public URL, installed apps and stored submissions are gone and the local .sakupa/site.json is removed, so the next deploy creates a brand-new site. A subscribed site cannot be deleted: end the subscription first (change \u2192 period-end cancellation on Stripe); when it ends the site reverts to a free site and delete becomes available. Owner-only.",
7299
+ inputSchema: z7.object({
7300
+ confirmed: z7.boolean().optional().describe("true only from the exact decision arguments returned by the preview."),
7301
+ operationId: z7.string().optional().describe("Copied verbatim from the preview."),
7302
+ confirmation: confirmationSchema.optional().describe("Copied verbatim from the preview; the cloud rejects any drift.")
7303
+ }),
7304
+ outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
7305
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
7306
+ },
7307
+ withDecisionReentry("delete", async (args, call) => {
7308
+ try {
7309
+ const ctx = await withProjectDir(baseCtx, call);
7310
+ const site = requireSiteFile(ctx);
7311
+ if (args.confirmed === true) {
7312
+ if (!args.operationId || !args.confirmation) {
7313
+ return structuredToolResult({
7314
+ schemaVersion: 1,
7315
+ outcome: "blocked",
7316
+ resultCode: "delete_preview_required",
7317
+ summary: summaryMarkdown({
7318
+ title: "Nothing was deleted \u2014 run the preview first",
7319
+ lead: "A confirmed delete must carry the exact operationId and confirmation from the preview. Call delete with no arguments, show the preview to the user, then reuse its decision arguments.",
7320
+ next: ["`delete` with no arguments"]
7321
+ }),
7322
+ data: { siteId: site.siteId },
7323
+ nextActions: [{ tool: "delete", arguments: {}, allowed: true }]
7324
+ });
7325
+ }
7326
+ const result = await ctx.client.deleteSite(site.siteId, site.credential, {
7327
+ operationId: args.operationId,
7328
+ confirmation: args.confirmation
7329
+ });
7330
+ deleteSiteFile(ctx.projectDir);
7331
+ removeCreation(site.siteId);
7332
+ return structuredToolResult({
7333
+ schemaVersion: 1,
7334
+ outcome: "completed",
7335
+ resultCode: result.alreadyDeleted ? "site_already_deleted" : "site_deleted",
7336
+ summary: summaryMarkdown({
7337
+ title: result.alreadyDeleted ? "Site was already deleted" : "Site deleted",
7338
+ lead: `${site.url ?? site.siteId} no longer serves anything (visitors get HTTP 410). The local credential file .sakupa/site.json was removed; this project is no longer bound to any site.`,
7339
+ facts: [
7340
+ ["Site ID", result.siteId],
7341
+ ["Public URL released", site.url],
7342
+ [
7343
+ "Custom hostnames removed",
7344
+ result.removedHostnames.length ? result.removedHostnames.join(", ") : "none"
7345
+ ],
7346
+ ["Serving cleanup still pending", result.servingDeletionPending]
7347
+ ],
7348
+ notes: [
7349
+ "Installed apps and stored form submissions were deleted with the site.",
7350
+ "The free-site allowance this site used on its deployment network is released."
7351
+ ],
7352
+ next: ["`deploy` to publish a brand-new site (new URL, new credential)"]
7353
+ }),
7354
+ data: {
7355
+ ...result,
7356
+ projectDir: ctx.projectDir,
7357
+ localSiteFileRemoved: true,
7358
+ releasedUrl: site.url
7359
+ },
7360
+ nextActions: []
7361
+ });
7362
+ }
7363
+ const operationId = randomUUID7();
7364
+ const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
7365
+ operationId
7366
+ });
7367
+ const c = preview.consequences;
7368
+ if (c.requiresFreeModeBeforeDelete) {
7369
+ const cancellationScheduled = preview.confirmation.expectedCancelAtPeriodEnd === true;
7370
+ return structuredToolResult({
7371
+ schemaVersion: 1,
7372
+ outcome: "blocked",
7373
+ resultCode: "delete_requires_free_mode",
7374
+ summary: summaryMarkdown({
7375
+ title: "This site is subscription-backed \u2014 it cannot be deleted yet",
7376
+ lead: "Nothing was changed. Sakupa deletes only FREE sites. End the subscription first; when the paid period ends the site automatically becomes a free 30-day site, and delete becomes available.",
7377
+ facts: [
7378
+ ["Site", site.url ?? site.siteId],
7379
+ ["Plan", preview.confirmation.expectedPlan],
7380
+ ["Period-end cancellation already scheduled", cancellationScheduled ? "yes" : "no"],
7381
+ [
7382
+ "Paid access ends",
7383
+ c.currentPaidAccessEndsAt ? timestampForAgent(c.currentPaidAccessEndsAt) : void 0
7384
+ ],
7385
+ [
7386
+ "Custom hostnames removed when it reverts",
7387
+ c.removesCustomHostnames.join(", ") || "none"
7388
+ ]
7389
+ ],
7390
+ steps: cancellationScheduled ? [
7391
+ "Wait for the paid period to end; Sakupa then reverts the site to a free 30-day site (content kept, custom domains removed).",
7392
+ "Call delete again at that point \u2014 or simply let the free site expire."
7393
+ ] : [
7394
+ "Call change and open the Stripe-hosted link; the user chooses period-end cancellation there (Sakupa never cancels on its own).",
7395
+ "When the paid period ends, Sakupa reverts the site to a free 30-day site (content kept, custom domains removed).",
7396
+ "Call delete again at that point \u2014 or simply let the free site expire."
7397
+ ],
7398
+ next: cancellationScheduled ? ["`billing`"] : ["`change`", "`billing`"]
7399
+ }),
7400
+ data: {
7401
+ siteId: site.siteId,
7402
+ operationId: preview.operationId,
7403
+ consequences: c,
7404
+ cancellationScheduled
7405
+ },
7406
+ nextActions: [
7407
+ { tool: cancellationScheduled ? "billing" : "change", arguments: {}, allowed: true }
7408
+ ]
7409
+ });
7410
+ }
7411
+ const confirmArguments = {
7412
+ confirmed: true,
7413
+ operationId: preview.operationId,
7414
+ confirmation: preview.confirmation
7415
+ };
7416
+ return presentDecision(baseCtx.decisions, call, "delete", {
7417
+ resultCode: "delete_confirmation_required",
7418
+ summary: summaryMarkdown({
7419
+ title: `Delete ${site.url ?? site.siteId}? Nothing was changed.`,
7420
+ lead: "This is a preview. Deleting cannot be undone: the content, the public URL, installed apps and stored form submissions disappear at once, and the local credential file is removed.",
7421
+ facts: [
7422
+ ["Site ID", site.siteId],
7423
+ ["Public URL", site.url],
7424
+ ["Permanent URL released", c.releasesPermanentUrl],
7425
+ ["Custom hostnames removed", c.removesCustomHostnames.join(", ") || "none"],
7426
+ ["Exact confirm arguments", JSON.stringify(confirmArguments)]
7427
+ ],
7428
+ notes: [
7429
+ "After deletion this project is unbound; the next deploy creates a brand-new site with a new URL and credential.",
7430
+ "To keep the URL but replace the content, deploy again instead of deleting."
7431
+ ]
7432
+ }),
7433
+ data: {
7434
+ siteId: site.siteId,
7435
+ operationId: preview.operationId,
7436
+ consequences: c,
7437
+ confirmation: preview.confirmation,
7438
+ confirmArguments
7439
+ },
7440
+ prompt: `Delete the site ${site.url ?? site.siteId} now? This cannot be undone.`,
7441
+ options: [
7442
+ callToolDecisionOption({
7443
+ id: "delete_site",
7444
+ label: `Delete ${site.url ?? site.siteId}`,
7445
+ description: "Remove the site, its content, apps and submissions, and release the URL.",
7446
+ consequences: [
7447
+ "Visitors get HTTP 410 immediately.",
7448
+ "The local credential file is removed; this project is unbound."
7449
+ ],
7450
+ tool: "delete",
7451
+ arguments: confirmArguments,
7452
+ reasonCode: "explicit_site_delete_confirmation"
7453
+ }),
7454
+ noActionDecisionOption({ description: "Keep the site exactly as it is." })
7455
+ ],
7456
+ legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
7457
+ });
7458
+ } catch (error) {
7459
+ return toolError(error);
7460
+ }
7461
+ })
7462
+ );
7463
+ }
7464
+
7259
7465
  // src/transport.ts
7260
7466
  var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
7261
7467
  var DEFAULT_UPLOAD_TIMEOUT_MS = 3e4;
@@ -7416,7 +7622,7 @@ Workflow:
7416
7622
  (Vite/Vue/React/Svelte/Astro/Next static export/Nuxt generate), run the build LOCALLY first,
7417
7623
  then re-run analyze.
7418
7624
  2. deploy \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
7419
- site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
7625
+ site (public URL ${hostPattern}, valid 30 days, free banner shown) and stores the
7420
7626
  management credential in .sakupa/site.json. Deploying again updates the site and refreshes
7421
7627
  its validity; refresh extends validity without uploading; status shows the
7422
7628
  current deployment and serving state at any time. Every update checks the credential's
@@ -7425,7 +7631,7 @@ Workflow:
7425
7631
  3. To keep the site live beyond the free period, subscribe it to a monthly hosting plan (plans
7426
7632
  shows the catalog; subscribe -> Stripe-hosted checkout;
7427
7633
  water/personal/share/business). While the subscription remains active, the
7428
- ${hostPattern} URL stays live without the free 24-hour expiry. Usage over the chosen plan
7634
+ ${hostPattern} URL stays live without the free 30-day expiry. Usage over the chosen plan
7429
7635
  shows an over-limit notice by default. An external AI may periodically query usage and
7430
7636
  recommend a plan, but Sakupa never changes a subscription automatically.
7431
7637
  4. Optionally bind a custom domain to the subscribed site (bind): an included extra
@@ -7436,13 +7642,20 @@ Workflow:
7436
7642
  until the new domain's www is confirmed live, then it is replaced automatically.
7437
7643
  Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
7438
7644
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
7439
- 24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
7645
+ 30-day site and removes paid data after Stripe sends the signed final-cancellation webhook.
7440
7646
  Recovery writes the new local credential before downloading content. If a session stops after
7441
7647
  .sakupa/site.json exists, NEVER repeat DNS recovery: resume with recover action "download".
7442
7648
  5. If any Sakupa operation is difficult or fails, call help FIRST. support handles billing,
7443
7649
  payment, refund and other customer-service requests. report is the LAST resort only when
7444
7650
  help explicitly recommends a product bug report, and submission still requires user review.
7445
7651
 
7652
+ Deleting a site: delete with no arguments only previews and returns a decision; after the user
7653
+ confirms, call delete again with the exact preview arguments. A FREE site is deleted immediately
7654
+ (content, URL, apps and submissions gone; .sakupa/site.json removed; the next deploy creates a new
7655
+ site). A subscribed site cannot be deleted: end the subscription first (change \u2192 period-end
7656
+ cancellation on Stripe); when it ends the site reverts to a free 30-day site and delete becomes
7657
+ available. Never suggest delete as a way to free up the free-site allowance during deploy.
7658
+
7446
7659
  Decision-options contract: when a result contains decision, present EVERY numbered option from the
7447
7660
  tool to the user and wait for their selection. No option is selected by default. Never choose from
7448
7661
  context, paraphrase a selection into different arguments, or invent another option. After the user
@@ -7599,6 +7812,7 @@ function createSakupaMcpServer(opts) {
7599
7812
  registerBillingTools(server, ctx);
7600
7813
  registerCredentialTools(server, ctx);
7601
7814
  registerAppsTools(server, ctx);
7815
+ registerDeleteTools(server, ctx);
7602
7816
  registerHelpTools(server, ctx);
7603
7817
  return server;
7604
7818
  }
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@ var DEFAULT_API_BASE_URL = "https://api.sakupa.com";
4
4
  var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
5
5
  var TEST_ACCESS_HEADER = "x-sakupa-test-token";
6
6
  var CREDENTIAL_ROTATION_RECOMMEND_AFTER_SECONDS = 7 * 24 * 60 * 60;
7
- var FREE_SITE_TTL_HOURS = 24;
7
+ var FREE_SITE_TTL_DAYS = 30;
8
+ var FREE_SITE_TTL_HOURS = FREE_SITE_TTL_DAYS * 24;
8
9
  var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
9
10
  var FREE_ACTIVE_SITES_PER_IP = 3;
10
11
  var PAID_SITE_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
@@ -147,7 +148,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
147
148
  }
148
149
 
149
150
  // ../core/dist/domain/version.js
150
- var SAKUPA_MCP_VERSION = "1.4.0";
151
+ var SAKUPA_MCP_VERSION = "1.5.0";
151
152
 
152
153
  // ../core/dist/domain/errors.js
153
154
  var HTTP_STATUS = {
@@ -2242,7 +2243,8 @@ var TARGET_MCP_TOOL_NAMES = [
2242
2243
  "change",
2243
2244
  "support",
2244
2245
  "report",
2245
- "apps"
2246
+ "apps",
2247
+ "delete"
2246
2248
  ];
2247
2249
  var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
2248
2250
  schemaVersion: z.literal(1),
@@ -3083,15 +3085,15 @@ function strFromU8(dat, latin1) {
3083
3085
  var slzh = function(d, b) {
3084
3086
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
3085
3087
  };
3086
- var zh = function(d, b, z7) {
3088
+ var zh = function(d, b, z8) {
3087
3089
  var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
3088
- var _a2 = z64hs(d, es, efl, z7, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
3090
+ var _a2 = z64hs(d, es, efl, z8, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
3089
3091
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
3090
3092
  };
3091
- var z64hs = function(d, b, l, z7, sc, su, off) {
3093
+ var z64hs = function(d, b, l, z8, sc, su, off) {
3092
3094
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
3093
3095
  var nf = nsc + nsu + noff;
3094
- if (z7 && nf) {
3096
+ if (z8 && nf) {
3095
3097
  for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
3096
3098
  if (b2(d, b) == 1) {
3097
3099
  return [
@@ -3102,7 +3104,7 @@ var z64hs = function(d, b, l, z7, sc, su, off) {
3102
3104
  ];
3103
3105
  }
3104
3106
  }
3105
- if (z7 < 2)
3107
+ if (z8 < 2)
3106
3108
  err(13);
3107
3109
  }
3108
3110
  return [sc, su, off, 0];
@@ -3119,18 +3121,18 @@ function unzipSync(data, opts) {
3119
3121
  if (!c)
3120
3122
  return {};
3121
3123
  var o = b4(data, e + 16);
3122
- var z7 = b4(data, e - 20) == 117853008;
3123
- if (z7) {
3124
+ var z8 = b4(data, e - 20) == 117853008;
3125
+ if (z8) {
3124
3126
  var ze = b4(data, e - 12);
3125
- z7 = b4(data, ze) == 101075792;
3126
- if (z7) {
3127
+ z8 = b4(data, ze) == 101075792;
3128
+ if (z8) {
3127
3129
  c = b4(data, ze + 32);
3128
3130
  o = b4(data, ze + 48);
3129
3131
  }
3130
3132
  }
3131
3133
  var fltr = opts && opts.filter;
3132
3134
  for (var i = 0; i < c; ++i) {
3133
- var _a2 = zh(data, o, z7), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
3135
+ var _a2 = zh(data, o, z8), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
3134
3136
  o = no;
3135
3137
  if (!fltr || fltr({
3136
3138
  name: fn,
@@ -4529,7 +4531,7 @@ function registerTools(server, baseCtx) {
4529
4531
  "deploy",
4530
4532
  {
4531
4533
  title: "Deploy site",
4532
- description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4534
+ description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_DAYS} days, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4533
4535
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4534
4536
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4535
4537
  inputSchema: z2.object({
@@ -4546,7 +4548,7 @@ function registerTools(server, baseCtx) {
4546
4548
  "Override automatic SPA-fallback detection (single index.html + JS auto-enables rewriting unknown paths to index.html; multiple HTML pages auto-disable it). Pass only to force the behavior against the detected structure."
4547
4549
  ),
4548
4550
  publicConfirmed: z2.boolean().optional().describe(
4549
- "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
4551
+ "Required only for the first deployment: user explicitly confirmed creation of a public URL valid for the free-site window."
4550
4552
  ),
4551
4553
  reuseSiteUrl: z2.string().url().optional().describe(
4552
4554
  "Exact existing free-site URL selected by the user when the three-site free-site allowance is full. Never invent this value; copy it from deploy nextActions."
@@ -4818,9 +4820,10 @@ function registerTools(server, baseCtx) {
4818
4820
  const confirmArguments = { ...args, ...confirmation };
4819
4821
  return presentDecision(baseCtx.decisions, call, "deploy", {
4820
4822
  resultCode: "public_deployment_confirmation_required",
4821
- summary: `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Nothing has been uploaded or made public yet. The exact confirmation field is publicConfirmed: true.`,
4823
+ summary: `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_DAYS} days. Nothing has been uploaded or made public yet. The exact confirmation field is publicConfirmed: true.`,
4822
4824
  data: {
4823
4825
  publicUrlLifetimeHours: FREE_SITE_TTL_HOURS,
4826
+ publicUrlLifetimeDays: FREE_SITE_TTL_DAYS,
4824
4827
  confirmationField: "publicConfirmed",
4825
4828
  confirmation,
4826
4829
  confirmArguments
@@ -4830,7 +4833,7 @@ function registerTools(server, baseCtx) {
4830
4833
  callToolDecisionOption({
4831
4834
  id: "create_public_preview",
4832
4835
  label: "Create the public preview",
4833
- description: `Publish the selected files at a public URL for ${FREE_SITE_TTL_HOURS} hours.`,
4836
+ description: `Publish the selected files at a public URL for ${FREE_SITE_TTL_DAYS} days.`,
4834
4837
  consequences: ["Anyone with the generated URL can open the site."],
4835
4838
  tool: "deploy",
4836
4839
  arguments: confirmArguments,
@@ -5042,7 +5045,7 @@ function registerTools(server, baseCtx) {
5042
5045
  ["Credential path", ".sakupa/site.json"]
5043
5046
  ],
5044
5047
  notes: [
5045
- `This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; while a subscription remains active, this URL stays live without the free-site expiry. This is conditional on the subscription remaining active: do NOT describe the site as permanent or long-term, and do NOT say the subscription is bound to the site.`,
5048
+ `This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_DAYS} days. Deploying again or calling refresh extends the validity; while a subscription remains active, this URL stays live without the free-site expiry. This is conditional on the subscription remaining active: do NOT describe the site as permanent or long-term, and do NOT say the subscription is bound to the site.`,
5046
5049
  "The management credential was saved to the exact relative path .sakupa/site.json \u2014 preserve this complete path verbatim and never shorten it to site.json. Keep that file: it is the only way to manage this site.",
5047
5050
  ...[credentialGitReminder(ctx.projectDir)].filter((line) => line.trim().length > 0)
5048
5051
  ],
@@ -5162,7 +5165,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}
5162
5165
  ...credentialRotationResumed ? [
5163
5166
  "A previously confirmed credential rotation was resumed safely before this deploy; every older credential is revoked."
5164
5167
  ] : [],
5165
- finalized.mode === "free" ? `Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. While a subscription remains active, the site stays live without this free-site expiry.` : "This site is subscription-backed and has no free-site expiry while the subscription remains active.",
5168
+ finalized.mode === "free" ? `Reminder: free sites stay live for ${FREE_SITE_TTL_DAYS} days after the last deploy or refresh call. While a subscription remains active, the site stays live without this free-site expiry.` : "This site is subscription-backed and has no free-site expiry while the subscription remains active.",
5166
5169
  ...credentialSecurity?.rotationRecommended ? [
5167
5170
  `Optional security recommendation: this management credential was created at ${timestampForAgent(credentialSecurity.credentialCreatedAt)} and is older than 7 days. The deploy SUCCEEDED and rotation is not required. Ask the user whether they want to rotate; call rotate without confirmed:true to show the exact revocation preview. Never rotate automatically.`
5168
5171
  ] : []
@@ -5264,7 +5267,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}
5264
5267
  ],
5265
5268
  notes: [
5266
5269
  "NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy.",
5267
- `Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`
5270
+ `Free sites stay live for ${FREE_SITE_TTL_DAYS} days after each deploy or refresh.`
5268
5271
  ],
5269
5272
  next: ["`status`", "`deploy` to publish changed files"]
5270
5273
  }),
@@ -5316,7 +5319,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}
5316
5319
  "subscribe",
5317
5320
  {
5318
5321
  title: "Subscribe (Stripe Checkout)",
5319
- description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 24-hour expiry. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
5322
+ description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 30-day expiry. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
5320
5323
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5321
5324
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5322
5325
  inputSchema: z2.object({
@@ -6287,6 +6290,7 @@ var TOOL_TOPICS = [
6287
6290
  "support",
6288
6291
  "report",
6289
6292
  "apps",
6293
+ "delete",
6290
6294
  "help"
6291
6295
  ];
6292
6296
  var HELP_TOPICS = ["diagnose", "overview", "terminology", ...TOOL_TOPICS];
@@ -6397,6 +6401,18 @@ var TOOL_MANUALS = {
6397
6401
  nextStep: "Use the preview resumeWith arguments only after the user confirms.",
6398
6402
  terminology: ["credentialRotation"]
6399
6403
  },
6404
+ delete: {
6405
+ purpose: "Delete the bound FREE site after an explicit preview and confirmation.",
6406
+ sideEffects: "Preview is read-only. Confirmed deletion removes content, the public URL, installed apps, stored submissions and the local .sakupa/site.json.",
6407
+ preconditions: "A valid local site credential and a free site. A subscribed site is refused until the subscription ends (change \u2192 period-end cancellation) and the site reverts to free.",
6408
+ parameterNames: ["confirmed", "operationId", "confirmation"],
6409
+ parameters: "No arguments for the preview; confirmed=true with the exact operationId and confirmation copied from the preview decision.",
6410
+ warnings: [
6411
+ "Irreversible; the next deploy creates a brand-new site with a new URL.",
6412
+ "Never propose delete to free up the free-site allowance; use the deploy handoff decision instead."
6413
+ ],
6414
+ nextStep: "Present every decision option; after confirmation copy the option arguments verbatim."
6415
+ },
6400
6416
  plans: {
6401
6417
  purpose: "Read the authoritative hosting plan catalog and rules.",
6402
6418
  sideEffects: "Read-only public API request.",
@@ -7375,6 +7391,196 @@ function registerAppsTools(server, baseCtx) {
7375
7391
  );
7376
7392
  }
7377
7393
 
7394
+ // src/tools/delete.ts
7395
+ import { randomUUID as randomUUID7 } from "node:crypto";
7396
+ import { z as z7 } from "zod";
7397
+ var confirmationSchema = z7.object({
7398
+ siteId: z7.string(),
7399
+ expectedSiteUpdatedAt: z7.string(),
7400
+ expectedStatus: z7.string(),
7401
+ expectedMode: z7.enum(["free", "paid"]),
7402
+ expectedServingMode: z7.string(),
7403
+ expectedShortId: z7.string().optional(),
7404
+ expectedSubscriptionStatus: z7.string().optional(),
7405
+ expectedPlan: z7.string().optional(),
7406
+ expectedCancelAtPeriodEnd: z7.boolean().optional(),
7407
+ expectedCurrentPeriodEnd: z7.string().optional(),
7408
+ expectedLastDeploymentId: z7.string().optional(),
7409
+ expectedBoundHostnames: z7.array(z7.string()),
7410
+ acknowledge: z7.literal("delete_and_cancel_renewal")
7411
+ });
7412
+ function registerDeleteTools(server, baseCtx) {
7413
+ server.registerTool(
7414
+ "delete",
7415
+ {
7416
+ title: "Delete site",
7417
+ description: "Delete the site bound to this project. Without arguments it only previews (zero writes) and returns a decision; after the user confirms, call again with the exact preview arguments (confirmed: true, operationId, confirmation). A FREE site is deleted immediately: content, public URL, installed apps and stored submissions are gone and the local .sakupa/site.json is removed, so the next deploy creates a brand-new site. A subscribed site cannot be deleted: end the subscription first (change \u2192 period-end cancellation on Stripe); when it ends the site reverts to a free site and delete becomes available. Owner-only.",
7418
+ inputSchema: z7.object({
7419
+ confirmed: z7.boolean().optional().describe("true only from the exact decision arguments returned by the preview."),
7420
+ operationId: z7.string().optional().describe("Copied verbatim from the preview."),
7421
+ confirmation: confirmationSchema.optional().describe("Copied verbatim from the preview; the cloud rejects any drift.")
7422
+ }),
7423
+ outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
7424
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
7425
+ },
7426
+ withDecisionReentry("delete", async (args, call) => {
7427
+ try {
7428
+ const ctx = await withProjectDir(baseCtx, call);
7429
+ const site = requireSiteFile(ctx);
7430
+ if (args.confirmed === true) {
7431
+ if (!args.operationId || !args.confirmation) {
7432
+ return structuredToolResult({
7433
+ schemaVersion: 1,
7434
+ outcome: "blocked",
7435
+ resultCode: "delete_preview_required",
7436
+ summary: summaryMarkdown({
7437
+ title: "Nothing was deleted \u2014 run the preview first",
7438
+ lead: "A confirmed delete must carry the exact operationId and confirmation from the preview. Call delete with no arguments, show the preview to the user, then reuse its decision arguments.",
7439
+ next: ["`delete` with no arguments"]
7440
+ }),
7441
+ data: { siteId: site.siteId },
7442
+ nextActions: [{ tool: "delete", arguments: {}, allowed: true }]
7443
+ });
7444
+ }
7445
+ const result = await ctx.client.deleteSite(site.siteId, site.credential, {
7446
+ operationId: args.operationId,
7447
+ confirmation: args.confirmation
7448
+ });
7449
+ deleteSiteFile(ctx.projectDir);
7450
+ removeCreation(site.siteId);
7451
+ return structuredToolResult({
7452
+ schemaVersion: 1,
7453
+ outcome: "completed",
7454
+ resultCode: result.alreadyDeleted ? "site_already_deleted" : "site_deleted",
7455
+ summary: summaryMarkdown({
7456
+ title: result.alreadyDeleted ? "Site was already deleted" : "Site deleted",
7457
+ lead: `${site.url ?? site.siteId} no longer serves anything (visitors get HTTP 410). The local credential file .sakupa/site.json was removed; this project is no longer bound to any site.`,
7458
+ facts: [
7459
+ ["Site ID", result.siteId],
7460
+ ["Public URL released", site.url],
7461
+ [
7462
+ "Custom hostnames removed",
7463
+ result.removedHostnames.length ? result.removedHostnames.join(", ") : "none"
7464
+ ],
7465
+ ["Serving cleanup still pending", result.servingDeletionPending]
7466
+ ],
7467
+ notes: [
7468
+ "Installed apps and stored form submissions were deleted with the site.",
7469
+ "The free-site allowance this site used on its deployment network is released."
7470
+ ],
7471
+ next: ["`deploy` to publish a brand-new site (new URL, new credential)"]
7472
+ }),
7473
+ data: {
7474
+ ...result,
7475
+ projectDir: ctx.projectDir,
7476
+ localSiteFileRemoved: true,
7477
+ releasedUrl: site.url
7478
+ },
7479
+ nextActions: []
7480
+ });
7481
+ }
7482
+ const operationId = randomUUID7();
7483
+ const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
7484
+ operationId
7485
+ });
7486
+ const c = preview.consequences;
7487
+ if (c.requiresFreeModeBeforeDelete) {
7488
+ const cancellationScheduled = preview.confirmation.expectedCancelAtPeriodEnd === true;
7489
+ return structuredToolResult({
7490
+ schemaVersion: 1,
7491
+ outcome: "blocked",
7492
+ resultCode: "delete_requires_free_mode",
7493
+ summary: summaryMarkdown({
7494
+ title: "This site is subscription-backed \u2014 it cannot be deleted yet",
7495
+ lead: "Nothing was changed. Sakupa deletes only FREE sites. End the subscription first; when the paid period ends the site automatically becomes a free 30-day site, and delete becomes available.",
7496
+ facts: [
7497
+ ["Site", site.url ?? site.siteId],
7498
+ ["Plan", preview.confirmation.expectedPlan],
7499
+ ["Period-end cancellation already scheduled", cancellationScheduled ? "yes" : "no"],
7500
+ [
7501
+ "Paid access ends",
7502
+ c.currentPaidAccessEndsAt ? timestampForAgent(c.currentPaidAccessEndsAt) : void 0
7503
+ ],
7504
+ [
7505
+ "Custom hostnames removed when it reverts",
7506
+ c.removesCustomHostnames.join(", ") || "none"
7507
+ ]
7508
+ ],
7509
+ steps: cancellationScheduled ? [
7510
+ "Wait for the paid period to end; Sakupa then reverts the site to a free 30-day site (content kept, custom domains removed).",
7511
+ "Call delete again at that point \u2014 or simply let the free site expire."
7512
+ ] : [
7513
+ "Call change and open the Stripe-hosted link; the user chooses period-end cancellation there (Sakupa never cancels on its own).",
7514
+ "When the paid period ends, Sakupa reverts the site to a free 30-day site (content kept, custom domains removed).",
7515
+ "Call delete again at that point \u2014 or simply let the free site expire."
7516
+ ],
7517
+ next: cancellationScheduled ? ["`billing`"] : ["`change`", "`billing`"]
7518
+ }),
7519
+ data: {
7520
+ siteId: site.siteId,
7521
+ operationId: preview.operationId,
7522
+ consequences: c,
7523
+ cancellationScheduled
7524
+ },
7525
+ nextActions: [
7526
+ { tool: cancellationScheduled ? "billing" : "change", arguments: {}, allowed: true }
7527
+ ]
7528
+ });
7529
+ }
7530
+ const confirmArguments = {
7531
+ confirmed: true,
7532
+ operationId: preview.operationId,
7533
+ confirmation: preview.confirmation
7534
+ };
7535
+ return presentDecision(baseCtx.decisions, call, "delete", {
7536
+ resultCode: "delete_confirmation_required",
7537
+ summary: summaryMarkdown({
7538
+ title: `Delete ${site.url ?? site.siteId}? Nothing was changed.`,
7539
+ lead: "This is a preview. Deleting cannot be undone: the content, the public URL, installed apps and stored form submissions disappear at once, and the local credential file is removed.",
7540
+ facts: [
7541
+ ["Site ID", site.siteId],
7542
+ ["Public URL", site.url],
7543
+ ["Permanent URL released", c.releasesPermanentUrl],
7544
+ ["Custom hostnames removed", c.removesCustomHostnames.join(", ") || "none"],
7545
+ ["Exact confirm arguments", JSON.stringify(confirmArguments)]
7546
+ ],
7547
+ notes: [
7548
+ "After deletion this project is unbound; the next deploy creates a brand-new site with a new URL and credential.",
7549
+ "To keep the URL but replace the content, deploy again instead of deleting."
7550
+ ]
7551
+ }),
7552
+ data: {
7553
+ siteId: site.siteId,
7554
+ operationId: preview.operationId,
7555
+ consequences: c,
7556
+ confirmation: preview.confirmation,
7557
+ confirmArguments
7558
+ },
7559
+ prompt: `Delete the site ${site.url ?? site.siteId} now? This cannot be undone.`,
7560
+ options: [
7561
+ callToolDecisionOption({
7562
+ id: "delete_site",
7563
+ label: `Delete ${site.url ?? site.siteId}`,
7564
+ description: "Remove the site, its content, apps and submissions, and release the URL.",
7565
+ consequences: [
7566
+ "Visitors get HTTP 410 immediately.",
7567
+ "The local credential file is removed; this project is unbound."
7568
+ ],
7569
+ tool: "delete",
7570
+ arguments: confirmArguments,
7571
+ reasonCode: "explicit_site_delete_confirmation"
7572
+ }),
7573
+ noActionDecisionOption({ description: "Keep the site exactly as it is." })
7574
+ ],
7575
+ legacyUserAction: { type: "confirm_in_mcp", provider: "sakupa" }
7576
+ });
7577
+ } catch (error) {
7578
+ return toolError(error);
7579
+ }
7580
+ })
7581
+ );
7582
+ }
7583
+
7378
7584
  // src/server.ts
7379
7585
  import { resolve as resolve6 } from "node:path";
7380
7586
  var instructionsFor = (hostPattern) => `Sakupa publishes AI-made static websites. AI-made pages, live in seconds.
@@ -7384,7 +7590,7 @@ Workflow:
7384
7590
  (Vite/Vue/React/Svelte/Astro/Next static export/Nuxt generate), run the build LOCALLY first,
7385
7591
  then re-run analyze.
7386
7592
  2. deploy \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
7387
- site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
7593
+ site (public URL ${hostPattern}, valid 30 days, free banner shown) and stores the
7388
7594
  management credential in .sakupa/site.json. Deploying again updates the site and refreshes
7389
7595
  its validity; refresh extends validity without uploading; status shows the
7390
7596
  current deployment and serving state at any time. Every update checks the credential's
@@ -7393,7 +7599,7 @@ Workflow:
7393
7599
  3. To keep the site live beyond the free period, subscribe it to a monthly hosting plan (plans
7394
7600
  shows the catalog; subscribe -> Stripe-hosted checkout;
7395
7601
  water/personal/share/business). While the subscription remains active, the
7396
- ${hostPattern} URL stays live without the free 24-hour expiry. Usage over the chosen plan
7602
+ ${hostPattern} URL stays live without the free 30-day expiry. Usage over the chosen plan
7397
7603
  shows an over-limit notice by default. An external AI may periodically query usage and
7398
7604
  recommend a plan, but Sakupa never changes a subscription automatically.
7399
7605
  4. Optionally bind a custom domain to the subscribed site (bind): an included extra
@@ -7404,13 +7610,20 @@ Workflow:
7404
7610
  until the new domain's www is confirmed live, then it is replaced automatically.
7405
7611
  Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
7406
7612
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
7407
- 24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
7613
+ 30-day site and removes paid data after Stripe sends the signed final-cancellation webhook.
7408
7614
  Recovery writes the new local credential before downloading content. If a session stops after
7409
7615
  .sakupa/site.json exists, NEVER repeat DNS recovery: resume with recover action "download".
7410
7616
  5. If any Sakupa operation is difficult or fails, call help FIRST. support handles billing,
7411
7617
  payment, refund and other customer-service requests. report is the LAST resort only when
7412
7618
  help explicitly recommends a product bug report, and submission still requires user review.
7413
7619
 
7620
+ Deleting a site: delete with no arguments only previews and returns a decision; after the user
7621
+ confirms, call delete again with the exact preview arguments. A FREE site is deleted immediately
7622
+ (content, URL, apps and submissions gone; .sakupa/site.json removed; the next deploy creates a new
7623
+ site). A subscribed site cannot be deleted: end the subscription first (change \u2192 period-end
7624
+ cancellation on Stripe); when it ends the site reverts to a free 30-day site and delete becomes
7625
+ available. Never suggest delete as a way to free up the free-site allowance during deploy.
7626
+
7414
7627
  Decision-options contract: when a result contains decision, present EVERY numbered option from the
7415
7628
  tool to the user and wait for their selection. No option is selected by default. Never choose from
7416
7629
  context, paraphrase a selection into different arguments, or invent another option. After the user
@@ -7567,6 +7780,7 @@ function createSakupaMcpServer(opts) {
7567
7780
  registerBillingTools(server, ctx);
7568
7781
  registerCredentialTools(server, ctx);
7569
7782
  registerAppsTools(server, ctx);
7783
+ registerDeleteTools(server, ctx);
7570
7784
  registerHelpTools(server, ctx);
7571
7785
  return server;
7572
7786
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "mcpName": "io.github.myerwang/sakupa",
5
5
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
6
6
  "homepage": "https://sakupa.com/manual/",