@sakupa/mcp 0.7.2 → 0.7.4

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 +106 -33
  2. package/dist/index.js +107 -34
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -128,7 +128,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
128
128
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
129
129
 
130
130
  // ../core/dist/domain/version.js
131
- var SAKUPA_MCP_VERSION = "0.7.2";
131
+ var SAKUPA_MCP_VERSION = "0.7.4";
132
132
 
133
133
  // ../core/dist/domain/errors.js
134
134
  var HTTP_STATUS = {
@@ -444,6 +444,9 @@ function safeDecode(bytes) {
444
444
  }
445
445
  }
446
446
 
447
+ // ../core/dist/domain/credentials.js
448
+ var CREDENTIAL_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
449
+
447
450
  // ../core/dist/domain/hashing.js
448
451
  async function sha256Hex(bytes) {
449
452
  const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
@@ -458,10 +461,6 @@ var CREDENTIAL_HEADER = "x-sakupa-credential";
458
461
  var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
459
462
  var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
460
463
 
461
- // ../core/dist/services/sites.js
462
- var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
463
- var utf8Encoder = new TextEncoder();
464
-
465
464
  // ../core/dist/services/subscriptions.js
466
465
  var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
467
466
 
@@ -1128,20 +1127,42 @@ var SITE_FILE = "site.json";
1128
1127
  function siteFilePath(projectDir) {
1129
1128
  return join2(projectDir, SITE_DIR, SITE_FILE);
1130
1129
  }
1131
- function readSiteFile(projectDir) {
1130
+ function loadSiteFile(projectDir) {
1132
1131
  const path = siteFilePath(projectDir);
1132
+ if (!existsSync(path)) return { kind: "absent" };
1133
1133
  let raw;
1134
1134
  try {
1135
1135
  raw = readFileSync(path, "utf8");
1136
- } catch {
1137
- return null;
1136
+ } catch (err) {
1137
+ return {
1138
+ kind: "corrupted",
1139
+ problem: `the file exists but could not be read (${err instanceof Error ? err.message : String(err)})`
1140
+ };
1138
1141
  }
1142
+ let parsed;
1139
1143
  try {
1140
- const parsed = JSON.parse(raw);
1141
- if (typeof parsed !== "object" || parsed === null) return null;
1142
- if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) return null;
1143
- if (typeof parsed.credential !== "string" || parsed.credential.length === 0) return null;
1144
+ parsed = JSON.parse(raw);
1145
+ } catch {
1146
+ return { kind: "corrupted", problem: "the file exists but is not valid JSON" };
1147
+ }
1148
+ if (typeof parsed !== "object" || parsed === null) {
1149
+ return { kind: "corrupted", problem: "the file does not contain a JSON object" };
1150
+ }
1151
+ if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) {
1152
+ return { kind: "corrupted", problem: "the siteId field is missing or empty" };
1153
+ }
1154
+ if (typeof parsed.credential !== "string" || parsed.credential.length === 0) {
1155
+ return { kind: "corrupted", problem: "the credential field is missing or empty" };
1156
+ }
1157
+ if (!CREDENTIAL_PATTERN.test(parsed.credential)) {
1144
1158
  return {
1159
+ kind: "corrupted",
1160
+ problem: "the credential does not match the shape Sakupa issues (sk_ followed by exactly 43 URL-safe base64 characters) \u2014 it looks locally altered or damaged"
1161
+ };
1162
+ }
1163
+ return {
1164
+ kind: "ok",
1165
+ file: {
1145
1166
  siteId: parsed.siteId,
1146
1167
  credential: parsed.credential,
1147
1168
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : "",
@@ -1149,12 +1170,26 @@ function readSiteFile(projectDir) {
1149
1170
  ...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
1150
1171
  ...typeof parsed.url === "string" ? { url: parsed.url } : {},
1151
1172
  ...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
1152
- };
1153
- } catch {
1154
- return null;
1155
- }
1173
+ }
1174
+ };
1156
1175
  }
1157
- function writeSiteFile(projectDir, file) {
1176
+ function siteFileRecoveryGuidance(projectDir) {
1177
+ return `The site itself is intact on the server; only the local binding file (${siteFilePath(projectDir)}) is the problem. Restore the file (from a backup or by undoing the local edit). Do NOT delete it to work around the error: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site. Publishing this project as a brand-NEW site requires the user to manually delete the .sakupa directory first \u2014 the tool will never overwrite it.`;
1178
+ }
1179
+ function writeSiteFile(projectDir, file, opts = {}) {
1180
+ if (opts.allowReplace !== true) {
1181
+ const existing = loadSiteFile(projectDir);
1182
+ if (existing.kind === "corrupted") {
1183
+ throw new Error(
1184
+ `Refusing to overwrite ${siteFilePath(projectDir)}: ${existing.problem}. ` + siteFileRecoveryGuidance(projectDir)
1185
+ );
1186
+ }
1187
+ if (existing.kind === "ok" && existing.file.siteId !== file.siteId) {
1188
+ throw new Error(
1189
+ `Refusing to overwrite ${siteFilePath(projectDir)}: it already binds this project to site ${existing.file.siteId}. ` + siteFileRecoveryGuidance(projectDir)
1190
+ );
1191
+ }
1192
+ }
1158
1193
  const dir = join2(projectDir, SITE_DIR);
1159
1194
  mkdirSync(dir, { recursive: true });
1160
1195
  const path = join2(dir, SITE_FILE);
@@ -1232,14 +1267,20 @@ function structuredToolResult(envelope) {
1232
1267
 
1233
1268
  // src/tools/context.ts
1234
1269
  function requireSiteFile(ctx) {
1235
- const file = readSiteFile(ctx.projectDir);
1236
- if (!file) {
1270
+ const state = loadSiteFile(ctx.projectDir);
1271
+ if (state.kind === "corrupted") {
1272
+ throw new SakupaError(
1273
+ "invalid_request",
1274
+ `.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
1275
+ );
1276
+ }
1277
+ if (state.kind === "absent") {
1237
1278
  throw new SakupaError(
1238
1279
  "not_found",
1239
1280
  `No .sakupa/site.json found in ${ctx.projectDir}. This project has no Sakupa site binding yet \u2014 run deploy_site first to publish it (the management credential will be stored locally in .sakupa/site.json). If this was a paid custom-domain site whose project file was lost, use recover_domain_site instead.`
1240
1281
  );
1241
1282
  }
1242
- return file;
1283
+ return state.file;
1243
1284
  }
1244
1285
  function toolError(e) {
1245
1286
  const errorCode = isSakupaError(e) ? e.code : "internal";
@@ -1460,7 +1501,18 @@ Next action: ${analysis.suggestedNextAction}`,
1460
1501
  const files = analysis.files;
1461
1502
  const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
1462
1503
  const manifest = await buildHashedManifest(files, outputAbs);
1463
- const existing = readSiteFile(ctx.projectDir);
1504
+ const siteFileState = loadSiteFile(ctx.projectDir);
1505
+ if (siteFileState.kind === "corrupted") {
1506
+ return text(
1507
+ "site_file_corrupted",
1508
+ `This project's .sakupa/site.json is damaged: ${siteFileState.problem}.
1509
+
1510
+ ` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
1511
+ { problem: siteFileState.problem },
1512
+ "blocked"
1513
+ );
1514
+ }
1515
+ const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
1464
1516
  if (!existing && args.publicConfirmed !== true) {
1465
1517
  return text(
1466
1518
  "public_deployment_confirmation_required",
@@ -1540,6 +1592,16 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1540
1592
  try {
1541
1593
  update = await updateOnce(false);
1542
1594
  } catch (e) {
1595
+ if (isSakupaError(e) && e.code === "unauthorized") {
1596
+ return text(
1597
+ "credential_mismatch",
1598
+ `The server rejected the credential in .sakupa/site.json for site ${existing.siteId} \u2014 the local file no longer matches the server-side verifier (most often a local edit or merge damage).
1599
+
1600
+ ` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
1601
+ { siteId: existing.siteId },
1602
+ "blocked"
1603
+ );
1604
+ }
1543
1605
  if (!isIncrementalReuseFailure(e)) throw e;
1544
1606
  update = await updateOnce(true);
1545
1607
  }
@@ -1890,13 +1952,17 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
1890
1952
  const res = await ctx.client.completeRecovery(args.verificationId, {
1891
1953
  ...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
1892
1954
  });
1893
- writeSiteFile(ctx.projectDir, {
1894
- siteId: res.siteId,
1895
- ...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
1896
- credential: res.credential,
1897
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1898
- apiBaseUrl: ctx.apiBaseUrl
1899
- });
1955
+ writeSiteFile(
1956
+ ctx.projectDir,
1957
+ {
1958
+ siteId: res.siteId,
1959
+ ...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
1960
+ credential: res.credential,
1961
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1962
+ apiBaseUrl: ctx.apiBaseUrl
1963
+ },
1964
+ { allowReplace: true }
1965
+ );
1900
1966
  return text(
1901
1967
  "domain_recovery_completed",
1902
1968
  `Recovery complete.
@@ -1973,7 +2039,8 @@ ${res.archiveUrl}`,
1973
2039
  },
1974
2040
  async (args) => {
1975
2041
  try {
1976
- const site = readSiteFile(ctx.projectDir);
2042
+ const siteState = loadSiteFile(ctx.projectDir);
2043
+ const site = siteState.kind === "ok" ? siteState.file : null;
1977
2044
  const diagnostics = {
1978
2045
  toolName: args.toolName,
1979
2046
  ...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
@@ -2301,9 +2368,11 @@ Workflow:
2301
2368
  2. deploy_site \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
2302
2369
  site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
2303
2370
  management credential in .sakupa/site.json. Deploying again updates the site and refreshes
2304
- its validity; refresh_site extends validity without uploading.
2305
- 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
2306
- Stripe-hosted checkout; water/personal/share/business). Paying makes the
2371
+ its validity; refresh_site extends validity without uploading; site_status shows the
2372
+ current deployment and serving state at any time.
2373
+ 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (list_billing_plans
2374
+ shows the catalog; subscribe_site -> Stripe-hosted checkout;
2375
+ water/personal/share/business). Paying makes the
2307
2376
  ${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
2308
2377
  shows an over-limit notice by default. An external AI may periodically query usage and
2309
2378
  recommend a plan, but Sakupa never changes a subscription automatically.
@@ -2311,9 +2380,13 @@ Workflow:
2311
2380
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
2312
2381
  first verified request wins; unverified requests expire after 72 hours. billing_status,
2313
2382
  change_subscription_plan, manage_billing and recover_domain_site manage the paid
2314
- lifecycle. Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2383
+ lifecycle; unbind_domain removes the custom domain (the permanent URL keeps serving) and
2384
+ delete_site tears the whole site down after explicit confirmation.
2385
+ Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2315
2386
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
2316
2387
  24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
2388
+ 5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
2389
+ sanitized diagnostic report after the user explicitly confirms it.
2317
2390
 
2318
2391
  Safety boundaries:
2319
2392
  - Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
package/dist/index.js CHANGED
@@ -123,7 +123,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
123
123
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
124
124
 
125
125
  // ../core/dist/domain/version.js
126
- var SAKUPA_MCP_VERSION = "0.7.2";
126
+ var SAKUPA_MCP_VERSION = "0.7.4";
127
127
 
128
128
  // ../core/dist/domain/errors.js
129
129
  var HTTP_STATUS = {
@@ -439,6 +439,9 @@ function safeDecode(bytes) {
439
439
  }
440
440
  }
441
441
 
442
+ // ../core/dist/domain/credentials.js
443
+ var CREDENTIAL_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
444
+
442
445
  // ../core/dist/domain/hashing.js
443
446
  async function sha256Hex(bytes) {
444
447
  const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
@@ -453,10 +456,6 @@ var CREDENTIAL_HEADER = "x-sakupa-credential";
453
456
  var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
454
457
  var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
455
458
 
456
- // ../core/dist/services/sites.js
457
- var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
458
- var utf8Encoder = new TextEncoder();
459
-
460
459
  // ../core/dist/services/subscriptions.js
461
460
  var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
462
461
 
@@ -764,20 +763,42 @@ var SITE_FILE = "site.json";
764
763
  function siteFilePath(projectDir) {
765
764
  return join(projectDir, SITE_DIR, SITE_FILE);
766
765
  }
767
- function readSiteFile(projectDir) {
766
+ function loadSiteFile(projectDir) {
768
767
  const path = siteFilePath(projectDir);
768
+ if (!existsSync(path)) return { kind: "absent" };
769
769
  let raw;
770
770
  try {
771
771
  raw = readFileSync(path, "utf8");
772
- } catch {
773
- return null;
772
+ } catch (err) {
773
+ return {
774
+ kind: "corrupted",
775
+ problem: `the file exists but could not be read (${err instanceof Error ? err.message : String(err)})`
776
+ };
774
777
  }
778
+ let parsed;
775
779
  try {
776
- const parsed = JSON.parse(raw);
777
- if (typeof parsed !== "object" || parsed === null) return null;
778
- if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) return null;
779
- if (typeof parsed.credential !== "string" || parsed.credential.length === 0) return null;
780
+ parsed = JSON.parse(raw);
781
+ } catch {
782
+ return { kind: "corrupted", problem: "the file exists but is not valid JSON" };
783
+ }
784
+ if (typeof parsed !== "object" || parsed === null) {
785
+ return { kind: "corrupted", problem: "the file does not contain a JSON object" };
786
+ }
787
+ if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) {
788
+ return { kind: "corrupted", problem: "the siteId field is missing or empty" };
789
+ }
790
+ if (typeof parsed.credential !== "string" || parsed.credential.length === 0) {
791
+ return { kind: "corrupted", problem: "the credential field is missing or empty" };
792
+ }
793
+ if (!CREDENTIAL_PATTERN.test(parsed.credential)) {
780
794
  return {
795
+ kind: "corrupted",
796
+ problem: "the credential does not match the shape Sakupa issues (sk_ followed by exactly 43 URL-safe base64 characters) \u2014 it looks locally altered or damaged"
797
+ };
798
+ }
799
+ return {
800
+ kind: "ok",
801
+ file: {
781
802
  siteId: parsed.siteId,
782
803
  credential: parsed.credential,
783
804
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : "",
@@ -785,12 +806,26 @@ function readSiteFile(projectDir) {
785
806
  ...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
786
807
  ...typeof parsed.url === "string" ? { url: parsed.url } : {},
787
808
  ...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
788
- };
789
- } catch {
790
- return null;
791
- }
809
+ }
810
+ };
792
811
  }
793
- function writeSiteFile(projectDir, file) {
812
+ function siteFileRecoveryGuidance(projectDir) {
813
+ return `The site itself is intact on the server; only the local binding file (${siteFilePath(projectDir)}) is the problem. Restore the file (from a backup or by undoing the local edit). Do NOT delete it to work around the error: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site. Publishing this project as a brand-NEW site requires the user to manually delete the .sakupa directory first \u2014 the tool will never overwrite it.`;
814
+ }
815
+ function writeSiteFile(projectDir, file, opts = {}) {
816
+ if (opts.allowReplace !== true) {
817
+ const existing = loadSiteFile(projectDir);
818
+ if (existing.kind === "corrupted") {
819
+ throw new Error(
820
+ `Refusing to overwrite ${siteFilePath(projectDir)}: ${existing.problem}. ` + siteFileRecoveryGuidance(projectDir)
821
+ );
822
+ }
823
+ if (existing.kind === "ok" && existing.file.siteId !== file.siteId) {
824
+ throw new Error(
825
+ `Refusing to overwrite ${siteFilePath(projectDir)}: it already binds this project to site ${existing.file.siteId}. ` + siteFileRecoveryGuidance(projectDir)
826
+ );
827
+ }
828
+ }
794
829
  const dir = join(projectDir, SITE_DIR);
795
830
  mkdirSync(dir, { recursive: true });
796
831
  const path = join(dir, SITE_FILE);
@@ -1290,14 +1325,20 @@ function structuredToolResult(envelope) {
1290
1325
 
1291
1326
  // src/tools/context.ts
1292
1327
  function requireSiteFile(ctx) {
1293
- const file = readSiteFile(ctx.projectDir);
1294
- if (!file) {
1328
+ const state = loadSiteFile(ctx.projectDir);
1329
+ if (state.kind === "corrupted") {
1330
+ throw new SakupaError(
1331
+ "invalid_request",
1332
+ `.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
1333
+ );
1334
+ }
1335
+ if (state.kind === "absent") {
1295
1336
  throw new SakupaError(
1296
1337
  "not_found",
1297
1338
  `No .sakupa/site.json found in ${ctx.projectDir}. This project has no Sakupa site binding yet \u2014 run deploy_site first to publish it (the management credential will be stored locally in .sakupa/site.json). If this was a paid custom-domain site whose project file was lost, use recover_domain_site instead.`
1298
1339
  );
1299
1340
  }
1300
- return file;
1341
+ return state.file;
1301
1342
  }
1302
1343
  function toolError(e) {
1303
1344
  const errorCode = isSakupaError(e) ? e.code : "internal";
@@ -1522,7 +1563,18 @@ Next action: ${analysis.suggestedNextAction}`,
1522
1563
  const files = analysis.files;
1523
1564
  const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
1524
1565
  const manifest = await buildHashedManifest(files, outputAbs);
1525
- const existing = readSiteFile(ctx.projectDir);
1566
+ const siteFileState = loadSiteFile(ctx.projectDir);
1567
+ if (siteFileState.kind === "corrupted") {
1568
+ return text(
1569
+ "site_file_corrupted",
1570
+ `This project's .sakupa/site.json is damaged: ${siteFileState.problem}.
1571
+
1572
+ ` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
1573
+ { problem: siteFileState.problem },
1574
+ "blocked"
1575
+ );
1576
+ }
1577
+ const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
1526
1578
  if (!existing && args.publicConfirmed !== true) {
1527
1579
  return text(
1528
1580
  "public_deployment_confirmation_required",
@@ -1602,6 +1654,16 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1602
1654
  try {
1603
1655
  update = await updateOnce(false);
1604
1656
  } catch (e) {
1657
+ if (isSakupaError(e) && e.code === "unauthorized") {
1658
+ return text(
1659
+ "credential_mismatch",
1660
+ `The server rejected the credential in .sakupa/site.json for site ${existing.siteId} \u2014 the local file no longer matches the server-side verifier (most often a local edit or merge damage).
1661
+
1662
+ ` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
1663
+ { siteId: existing.siteId },
1664
+ "blocked"
1665
+ );
1666
+ }
1605
1667
  if (!isIncrementalReuseFailure(e)) throw e;
1606
1668
  update = await updateOnce(true);
1607
1669
  }
@@ -1952,13 +2014,17 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
1952
2014
  const res = await ctx.client.completeRecovery(args.verificationId, {
1953
2015
  ...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
1954
2016
  });
1955
- writeSiteFile(ctx.projectDir, {
1956
- siteId: res.siteId,
1957
- ...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
1958
- credential: res.credential,
1959
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1960
- apiBaseUrl: ctx.apiBaseUrl
1961
- });
2017
+ writeSiteFile(
2018
+ ctx.projectDir,
2019
+ {
2020
+ siteId: res.siteId,
2021
+ ...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
2022
+ credential: res.credential,
2023
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2024
+ apiBaseUrl: ctx.apiBaseUrl
2025
+ },
2026
+ { allowReplace: true }
2027
+ );
1962
2028
  return text(
1963
2029
  "domain_recovery_completed",
1964
2030
  `Recovery complete.
@@ -2035,7 +2101,8 @@ ${res.archiveUrl}`,
2035
2101
  },
2036
2102
  async (args) => {
2037
2103
  try {
2038
- const site = readSiteFile(ctx.projectDir);
2104
+ const siteState = loadSiteFile(ctx.projectDir);
2105
+ const site = siteState.kind === "ok" ? siteState.file : null;
2039
2106
  const diagnostics = {
2040
2107
  toolName: args.toolName,
2041
2108
  ...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
@@ -2294,9 +2361,11 @@ Workflow:
2294
2361
  2. deploy_site \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
2295
2362
  site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
2296
2363
  management credential in .sakupa/site.json. Deploying again updates the site and refreshes
2297
- its validity; refresh_site extends validity without uploading.
2298
- 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
2299
- Stripe-hosted checkout; water/personal/share/business). Paying makes the
2364
+ its validity; refresh_site extends validity without uploading; site_status shows the
2365
+ current deployment and serving state at any time.
2366
+ 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (list_billing_plans
2367
+ shows the catalog; subscribe_site -> Stripe-hosted checkout;
2368
+ water/personal/share/business). Paying makes the
2300
2369
  ${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
2301
2370
  shows an over-limit notice by default. An external AI may periodically query usage and
2302
2371
  recommend a plan, but Sakupa never changes a subscription automatically.
@@ -2304,9 +2373,13 @@ Workflow:
2304
2373
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
2305
2374
  first verified request wins; unverified requests expire after 72 hours. billing_status,
2306
2375
  change_subscription_plan, manage_billing and recover_domain_site manage the paid
2307
- lifecycle. Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2376
+ lifecycle; unbind_domain removes the custom domain (the permanent URL keeps serving) and
2377
+ delete_site tears the whole site down after explicit confirmation.
2378
+ Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2308
2379
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
2309
2380
  24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
2381
+ 5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
2382
+ sanitized diagnostic report after the user explicitly confirms it.
2310
2383
 
2311
2384
  Safety boundaries:
2312
2385
  - Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
@@ -2352,7 +2425,7 @@ export {
2352
2425
  createSakupaMcpServer,
2353
2426
  deleteSiteFile,
2354
2427
  loadMcpRuntimeConfig,
2355
- readSiteFile,
2428
+ loadSiteFile,
2356
2429
  registerLifecycleTools,
2357
2430
  registerTools,
2358
2431
  requireSiteFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",