@sakupa/mcp 0.7.3 → 0.7.5

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 +109 -42
  2. package/dist/index.js +110 -43
  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.3";
131
+ var SAKUPA_MCP_VERSION = "0.7.5";
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";
@@ -1258,7 +1299,7 @@ function toolError(e) {
1258
1299
  ([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
1259
1300
  )
1260
1301
  ) : void 0;
1261
- const safeSummary = errorCode === "not_found" ? "\u6240\u9700\u7684\u672C\u5730\u9879\u76EE\u7ED1\u5B9A\u6216\u8D44\u6E90\u4E0D\u53EF\u7528\uFF1B\u5982\u679C\u672C\u5730\u6CA1\u6709 .sakupa/site.json\uFF0C\u8BF7\u5148\u8FD0\u884C deploy_site first\u3002" : errorCode === "unauthorized" ? "\u5F53\u524D\u64CD\u4F5C\u672A\u901A\u8FC7\u7AD9\u70B9\u6743\u9650\u6821\u9A8C\u3002" : errorCode === "invalid_request" || errorCode === "validation_failed" ? "\u8BF7\u6C42\u53C2\u6570\u6216\u672C\u5730\u9879\u76EE\u68C0\u67E5\u672A\u901A\u8FC7\u3002" : errorCode === "state_conflict" ? "\u8D44\u6E90\u72B6\u6001\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u72B6\u6001\u540E\u518D\u51B3\u5B9A\u4E0B\u4E00\u6B65\u3002" : errorCode === "confirmation_required" ? "\u8D44\u6E90\u6216\u8D26\u5355\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u65E7\u786E\u8BA4\u5DF2\u5931\u6548\uFF1B\u8BF7\u91CD\u65B0\u9884\u89C8\u540E\u518D\u786E\u8BA4\u3002" : errorCode === "payment_required" ? "\u8BE5\u64CD\u4F5C\u9700\u8981\u6709\u6548\u8BA2\u9605\uFF1B\u8BF7\u5148\u67E5\u8BE2\u8D26\u5355\u72B6\u6001\u3002" : errorCode === "rate_limited" ? "\u8BF7\u6C42\u9891\u7387\u5DF2\u8FBE\u5230\u670D\u52A1\u7AEF\u4E0A\u9650\uFF0C\u8BF7\u6309\u8FD4\u56DE\u7684\u7B49\u5F85\u65F6\u95F4\u540E\u91CD\u8BD5\u3002" : retryable ? "\u5916\u90E8\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\u6216\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : "\u64CD\u4F5C\u5931\u8D25\uFF1B\u672A\u8FD4\u56DE\u670D\u52A1\u7AEF\u5185\u90E8\u8BE6\u60C5\u3002";
1302
+ const safeSummary = errorCode === "not_found" ? "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first." : errorCode === "unauthorized" ? "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." : errorCode === "invalid_request" || errorCode === "validation_failed" ? "The request arguments or local project checks did not pass." : errorCode === "state_conflict" ? "The resource state has changed; re-query the current status before deciding the next step." : errorCode === "confirmation_required" ? "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot." : errorCode === "payment_required" ? "This operation requires an active subscription; check billing_status first." : errorCode === "rate_limited" ? "The server rate limit was reached; retry after the returned wait time." : retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.";
1262
1303
  const result = structuredToolResult({
1263
1304
  schemaVersion: 1,
1264
1305
  outcome: "failed",
@@ -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
  }
@@ -1784,13 +1846,13 @@ Full status:`, res);
1784
1846
  schemaVersion: 1,
1785
1847
  outcome: "waiting_user",
1786
1848
  resultCode: "site_billing_portal_ready",
1787
- summary: `\u5DF2\u521B\u5EFA\u6B64\u7AD9\u70B9\u7684 Stripe \u5BA2\u6237\u95E8\u6237\u77ED\u65F6\u94FE\u63A5\uFF1A${res2.portalUrl}\u3002\u4EFB\u4F55\u53D8\u66F4\u4ECD\u987B\u5728 Stripe \u9875\u9762\u5B8C\u6210\u3002`,
1849
+ summary: `Short-lived Stripe customer portal link created for this site: ${res2.portalUrl}. Any change still happens only on the Stripe-hosted page.`,
1788
1850
  data: { scope: args.scope, portalUrl: res2.portalUrl },
1789
1851
  userAction: {
1790
1852
  type: "open_url",
1791
1853
  provider: "stripe",
1792
1854
  url: res2.portalUrl,
1793
- expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u7BA1\u7406\u4ED8\u6B3E\u65B9\u5F0F\u3001\u53D1\u7968\u6216\u53D6\u6D88\u7EED\u8BA2"
1855
+ expectedOutcome: "The user manages payment methods, invoices, or cancels renewal on the Stripe-hosted page"
1794
1856
  },
1795
1857
  nextActions: [{ tool: "billing_status", allowed: true }]
1796
1858
  });
@@ -1800,7 +1862,7 @@ Full status:`, res);
1800
1862
  schemaVersion: 1,
1801
1863
  outcome: "waiting_user",
1802
1864
  resultCode: "public_billing_recovery_portal_ready",
1803
- summary: `Stripe \u516C\u5171\u90AE\u7BB1 OTP \u767B\u5F55\u9875\uFF1A${res.portalUrl}\u3002\u5B83\u4F7F\u7528 one-time passcode\uFF0Cdoes not recover the Sakupa key\uFF0C\u4E5F\u4E0D\u6388\u4E88\u7AD9\u70B9\u6743\u9650\uFF1B\u540C\u90AE\u7BB1\u5B58\u5728\u591A\u4E2A Customer \u65F6\uFF0CStripe \u53EF\u80FD\u53EA\u6253\u5F00 most recently created \u7684\u53EF\u7528\u8BB0\u5F55\u3002`,
1865
+ summary: `Stripe public email-OTP login page: ${res.portalUrl}. It uses a one-time passcode, does not recover the Sakupa key, and grants no site authority; when one email has several Customers, Stripe may open only the most recently created usable record.`,
1804
1866
  data: {
1805
1867
  scope: args.scope,
1806
1868
  portalUrl: res.portalUrl,
@@ -1811,7 +1873,7 @@ Full status:`, res);
1811
1873
  type: "open_url",
1812
1874
  provider: "stripe",
1813
1875
  url: res.portalUrl,
1814
- expectedOutcome: "\u7528\u6237\u7531 Stripe \u9A8C\u8BC1\u8D26\u5355\u90AE\u7BB1\u540E\u67E5\u770B\u5E76\u53D6\u6D88\u95E8\u6237\u4E2D\u663E\u793A\u7684\u8BA2\u9605"
1876
+ expectedOutcome: "The user verifies the billing email with Stripe, then reviews and cancels the subscription shown in the portal"
1815
1877
  },
1816
1878
  nextActions: []
1817
1879
  });
@@ -1875,7 +1937,7 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
1875
1937
  schemaVersion: 1,
1876
1938
  outcome: res2.status === "expired" ? "expired" : res2.readyToComplete ? "completed" : "pending_provider",
1877
1939
  resultCode: res2.status === "expired" ? "domain_recovery_expired" : res2.readyToComplete ? "domain_recovery_ready" : "domain_recovery_pending_dns",
1878
- summary: `DNS \u6062\u590D\u9A8C\u8BC1\u72B6\u6001\uFF1A${res2.status}`,
1940
+ summary: `DNS recovery verification status: ${res2.status}`,
1879
1941
  data: { recovery: res2 },
1880
1942
  nextActions: [
1881
1943
  {
@@ -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 } : {},
@@ -2033,7 +2100,7 @@ function registerBillingTools(server, ctx) {
2033
2100
  schemaVersion: 1,
2034
2101
  outcome: "completed",
2035
2102
  resultCode: "billing_catalog_returned",
2036
- summary: `\u5DF2\u8FD4\u56DE ${catalog.plans.length} \u4E2A\u6708\u4ED8\u65B9\u6848\uFF1BStripe \u6258\u7BA1\u9875\u9762\u662F\u4ED8\u8D39\u4E0E\u6539\u6863\u7684\u6700\u7EC8\u786E\u8BA4\u5165\u53E3\u3002`,
2103
+ summary: `Returned ${catalog.plans.length} monthly plans; the Stripe-hosted page is the final confirmation surface for payment and plan changes.`,
2037
2104
  data: { catalog },
2038
2105
  nextActions: [{ tool: "subscribe_site", allowed: true }]
2039
2106
  });
@@ -2066,13 +2133,13 @@ function registerBillingTools(server, ctx) {
2066
2133
  outcome: "waiting_user",
2067
2134
  resultCode: "stripe_plan_change_confirmation_required",
2068
2135
  operationId: args.operationId,
2069
- summary: `\u5DF2\u751F\u6210\u4ECE ${result.currentPlan} \u5230 ${result.targetPlan} \u7684 Stripe \u786E\u8BA4\u94FE\u63A5\uFF1B\u8BA2\u9605\u5C1A\u672A\u53D8\u66F4\u3002`,
2136
+ summary: `Stripe confirmation link created for ${result.currentPlan} -> ${result.targetPlan}; the subscription has NOT changed yet.`,
2070
2137
  data: { result },
2071
2138
  userAction: {
2072
2139
  type: "open_url",
2073
2140
  provider: "stripe",
2074
2141
  url: result.portalUrl,
2075
- expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u786E\u8BA4\u540E\uFF0C\u7531 webhook \u66F4\u65B0 Sakupa \u8BA2\u9605\u72B6\u6001"
2142
+ expectedOutcome: "After the user confirms on the Stripe-hosted page, the webhook updates the Sakupa subscription state"
2076
2143
  },
2077
2144
  nextActions: [{ tool: "billing_status", allowed: true }]
2078
2145
  });
@@ -2137,7 +2204,7 @@ function registerLifecycleTools(server, ctx) {
2137
2204
  outcome: "waiting_user",
2138
2205
  resultCode: "delete_site_confirmation_required",
2139
2206
  operationId: args.operationId,
2140
- summary: "\u5DF2\u8FD4\u56DE\u4E0E\u5F53\u524D\u7AD9\u70B9\u53CA\u8D26\u5355\u72B6\u6001\u7ED1\u5B9A\u7684\u5220\u9664\u540E\u679C\uFF1B\u786E\u8BA4\u540E\u5185\u5BB9\u548C\u6C38\u4E45\u5730\u5740\u4E0D\u53EF\u6062\u590D\u3002",
2207
+ summary: "Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable.",
2141
2208
  data: { preview },
2142
2209
  nextActions: [
2143
2210
  { tool: "delete_site", allowed: true, reasonCode: "exact_confirmation_required" }
@@ -2155,7 +2222,7 @@ function registerLifecycleTools(server, ctx) {
2155
2222
  outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2156
2223
  resultCode: "site_deleted",
2157
2224
  operationId: args.operationId,
2158
- summary: "\u7AD9\u70B9\u5DF2\u5220\u9664\uFF0C\u672C\u5730\u7BA1\u7406\u51ED\u8BC1\u6587\u4EF6\u5DF2\u79FB\u9664\u3002",
2225
+ summary: "Site deleted; the local management credential file was removed.",
2159
2226
  data: { result },
2160
2227
  nextActions: []
2161
2228
  });
@@ -2189,7 +2256,7 @@ function registerLifecycleTools(server, ctx) {
2189
2256
  outcome: "waiting_user",
2190
2257
  resultCode: "unbind_domain_confirmation_required",
2191
2258
  operationId: args.operationId,
2192
- summary: "\u5DF2\u8FD4\u56DE\u7CBE\u786E\u7ED1\u5B9A\u5FEB\u7167\uFF1B\u89E3\u7ED1\u53EA\u79FB\u9664\u81EA\u5B9A\u4E49\u57DF\u540D\uFF0C\u8BA2\u9605\u3001\u5185\u5BB9\u548C\u6C38\u4E45\u5730\u5740\u4FDD\u6301\u4E0D\u53D8\u3002",
2259
+ summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
2193
2260
  data: { preview },
2194
2261
  nextActions: [
2195
2262
  { tool: "unbind_domain", allowed: true, reasonCode: "exact_confirmation_required" }
@@ -2208,7 +2275,7 @@ function registerLifecycleTools(server, ctx) {
2208
2275
  outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2209
2276
  resultCode: "domain_unbound",
2210
2277
  operationId: args.operationId,
2211
- summary: "\u81EA\u5B9A\u4E49\u57DF\u540D\u5DF2\u89E3\u7ED1\uFF1B\u8BA2\u9605\u3001\u5DF2\u90E8\u7F72\u5185\u5BB9\u548C\u6C38\u4E45 Sakupa \u5730\u5740\u672A\u53D8\u3002",
2278
+ summary: "Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged.",
2212
2279
  data: { result },
2213
2280
  nextActions: [{ tool: "site_status", allowed: true }]
2214
2281
  });
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.3";
126
+ var SAKUPA_MCP_VERSION = "0.7.5";
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";
@@ -1316,7 +1357,7 @@ function toolError(e) {
1316
1357
  ([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
1317
1358
  )
1318
1359
  ) : void 0;
1319
- const safeSummary = errorCode === "not_found" ? "\u6240\u9700\u7684\u672C\u5730\u9879\u76EE\u7ED1\u5B9A\u6216\u8D44\u6E90\u4E0D\u53EF\u7528\uFF1B\u5982\u679C\u672C\u5730\u6CA1\u6709 .sakupa/site.json\uFF0C\u8BF7\u5148\u8FD0\u884C deploy_site first\u3002" : errorCode === "unauthorized" ? "\u5F53\u524D\u64CD\u4F5C\u672A\u901A\u8FC7\u7AD9\u70B9\u6743\u9650\u6821\u9A8C\u3002" : errorCode === "invalid_request" || errorCode === "validation_failed" ? "\u8BF7\u6C42\u53C2\u6570\u6216\u672C\u5730\u9879\u76EE\u68C0\u67E5\u672A\u901A\u8FC7\u3002" : errorCode === "state_conflict" ? "\u8D44\u6E90\u72B6\u6001\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u72B6\u6001\u540E\u518D\u51B3\u5B9A\u4E0B\u4E00\u6B65\u3002" : errorCode === "confirmation_required" ? "\u8D44\u6E90\u6216\u8D26\u5355\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u65E7\u786E\u8BA4\u5DF2\u5931\u6548\uFF1B\u8BF7\u91CD\u65B0\u9884\u89C8\u540E\u518D\u786E\u8BA4\u3002" : errorCode === "payment_required" ? "\u8BE5\u64CD\u4F5C\u9700\u8981\u6709\u6548\u8BA2\u9605\uFF1B\u8BF7\u5148\u67E5\u8BE2\u8D26\u5355\u72B6\u6001\u3002" : errorCode === "rate_limited" ? "\u8BF7\u6C42\u9891\u7387\u5DF2\u8FBE\u5230\u670D\u52A1\u7AEF\u4E0A\u9650\uFF0C\u8BF7\u6309\u8FD4\u56DE\u7684\u7B49\u5F85\u65F6\u95F4\u540E\u91CD\u8BD5\u3002" : retryable ? "\u5916\u90E8\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\u6216\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : "\u64CD\u4F5C\u5931\u8D25\uFF1B\u672A\u8FD4\u56DE\u670D\u52A1\u7AEF\u5185\u90E8\u8BE6\u60C5\u3002";
1360
+ const safeSummary = errorCode === "not_found" ? "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first." : errorCode === "unauthorized" ? "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." : errorCode === "invalid_request" || errorCode === "validation_failed" ? "The request arguments or local project checks did not pass." : errorCode === "state_conflict" ? "The resource state has changed; re-query the current status before deciding the next step." : errorCode === "confirmation_required" ? "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot." : errorCode === "payment_required" ? "This operation requires an active subscription; check billing_status first." : errorCode === "rate_limited" ? "The server rate limit was reached; retry after the returned wait time." : retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.";
1320
1361
  const result = structuredToolResult({
1321
1362
  schemaVersion: 1,
1322
1363
  outcome: "failed",
@@ -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
  }
@@ -1846,13 +1908,13 @@ Full status:`, res);
1846
1908
  schemaVersion: 1,
1847
1909
  outcome: "waiting_user",
1848
1910
  resultCode: "site_billing_portal_ready",
1849
- summary: `\u5DF2\u521B\u5EFA\u6B64\u7AD9\u70B9\u7684 Stripe \u5BA2\u6237\u95E8\u6237\u77ED\u65F6\u94FE\u63A5\uFF1A${res2.portalUrl}\u3002\u4EFB\u4F55\u53D8\u66F4\u4ECD\u987B\u5728 Stripe \u9875\u9762\u5B8C\u6210\u3002`,
1911
+ summary: `Short-lived Stripe customer portal link created for this site: ${res2.portalUrl}. Any change still happens only on the Stripe-hosted page.`,
1850
1912
  data: { scope: args.scope, portalUrl: res2.portalUrl },
1851
1913
  userAction: {
1852
1914
  type: "open_url",
1853
1915
  provider: "stripe",
1854
1916
  url: res2.portalUrl,
1855
- expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u7BA1\u7406\u4ED8\u6B3E\u65B9\u5F0F\u3001\u53D1\u7968\u6216\u53D6\u6D88\u7EED\u8BA2"
1917
+ expectedOutcome: "The user manages payment methods, invoices, or cancels renewal on the Stripe-hosted page"
1856
1918
  },
1857
1919
  nextActions: [{ tool: "billing_status", allowed: true }]
1858
1920
  });
@@ -1862,7 +1924,7 @@ Full status:`, res);
1862
1924
  schemaVersion: 1,
1863
1925
  outcome: "waiting_user",
1864
1926
  resultCode: "public_billing_recovery_portal_ready",
1865
- summary: `Stripe \u516C\u5171\u90AE\u7BB1 OTP \u767B\u5F55\u9875\uFF1A${res.portalUrl}\u3002\u5B83\u4F7F\u7528 one-time passcode\uFF0Cdoes not recover the Sakupa key\uFF0C\u4E5F\u4E0D\u6388\u4E88\u7AD9\u70B9\u6743\u9650\uFF1B\u540C\u90AE\u7BB1\u5B58\u5728\u591A\u4E2A Customer \u65F6\uFF0CStripe \u53EF\u80FD\u53EA\u6253\u5F00 most recently created \u7684\u53EF\u7528\u8BB0\u5F55\u3002`,
1927
+ summary: `Stripe public email-OTP login page: ${res.portalUrl}. It uses a one-time passcode, does not recover the Sakupa key, and grants no site authority; when one email has several Customers, Stripe may open only the most recently created usable record.`,
1866
1928
  data: {
1867
1929
  scope: args.scope,
1868
1930
  portalUrl: res.portalUrl,
@@ -1873,7 +1935,7 @@ Full status:`, res);
1873
1935
  type: "open_url",
1874
1936
  provider: "stripe",
1875
1937
  url: res.portalUrl,
1876
- expectedOutcome: "\u7528\u6237\u7531 Stripe \u9A8C\u8BC1\u8D26\u5355\u90AE\u7BB1\u540E\u67E5\u770B\u5E76\u53D6\u6D88\u95E8\u6237\u4E2D\u663E\u793A\u7684\u8BA2\u9605"
1938
+ expectedOutcome: "The user verifies the billing email with Stripe, then reviews and cancels the subscription shown in the portal"
1877
1939
  },
1878
1940
  nextActions: []
1879
1941
  });
@@ -1937,7 +1999,7 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
1937
1999
  schemaVersion: 1,
1938
2000
  outcome: res2.status === "expired" ? "expired" : res2.readyToComplete ? "completed" : "pending_provider",
1939
2001
  resultCode: res2.status === "expired" ? "domain_recovery_expired" : res2.readyToComplete ? "domain_recovery_ready" : "domain_recovery_pending_dns",
1940
- summary: `DNS \u6062\u590D\u9A8C\u8BC1\u72B6\u6001\uFF1A${res2.status}`,
2002
+ summary: `DNS recovery verification status: ${res2.status}`,
1941
2003
  data: { recovery: res2 },
1942
2004
  nextActions: [
1943
2005
  {
@@ -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 } : {},
@@ -2130,7 +2197,7 @@ function registerLifecycleTools(server, ctx) {
2130
2197
  outcome: "waiting_user",
2131
2198
  resultCode: "delete_site_confirmation_required",
2132
2199
  operationId: args.operationId,
2133
- summary: "\u5DF2\u8FD4\u56DE\u4E0E\u5F53\u524D\u7AD9\u70B9\u53CA\u8D26\u5355\u72B6\u6001\u7ED1\u5B9A\u7684\u5220\u9664\u540E\u679C\uFF1B\u786E\u8BA4\u540E\u5185\u5BB9\u548C\u6C38\u4E45\u5730\u5740\u4E0D\u53EF\u6062\u590D\u3002",
2200
+ summary: "Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable.",
2134
2201
  data: { preview },
2135
2202
  nextActions: [
2136
2203
  { tool: "delete_site", allowed: true, reasonCode: "exact_confirmation_required" }
@@ -2148,7 +2215,7 @@ function registerLifecycleTools(server, ctx) {
2148
2215
  outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2149
2216
  resultCode: "site_deleted",
2150
2217
  operationId: args.operationId,
2151
- summary: "\u7AD9\u70B9\u5DF2\u5220\u9664\uFF0C\u672C\u5730\u7BA1\u7406\u51ED\u8BC1\u6587\u4EF6\u5DF2\u79FB\u9664\u3002",
2218
+ summary: "Site deleted; the local management credential file was removed.",
2152
2219
  data: { result },
2153
2220
  nextActions: []
2154
2221
  });
@@ -2182,7 +2249,7 @@ function registerLifecycleTools(server, ctx) {
2182
2249
  outcome: "waiting_user",
2183
2250
  resultCode: "unbind_domain_confirmation_required",
2184
2251
  operationId: args.operationId,
2185
- summary: "\u5DF2\u8FD4\u56DE\u7CBE\u786E\u7ED1\u5B9A\u5FEB\u7167\uFF1B\u89E3\u7ED1\u53EA\u79FB\u9664\u81EA\u5B9A\u4E49\u57DF\u540D\uFF0C\u8BA2\u9605\u3001\u5185\u5BB9\u548C\u6C38\u4E45\u5730\u5740\u4FDD\u6301\u4E0D\u53D8\u3002",
2252
+ summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
2186
2253
  data: { preview },
2187
2254
  nextActions: [
2188
2255
  { tool: "unbind_domain", allowed: true, reasonCode: "exact_confirmation_required" }
@@ -2201,7 +2268,7 @@ function registerLifecycleTools(server, ctx) {
2201
2268
  outcome: result.servingDeletionPending ? "pending_provider" : "completed",
2202
2269
  resultCode: "domain_unbound",
2203
2270
  operationId: args.operationId,
2204
- summary: "\u81EA\u5B9A\u4E49\u57DF\u540D\u5DF2\u89E3\u7ED1\uFF1B\u8BA2\u9605\u3001\u5DF2\u90E8\u7F72\u5185\u5BB9\u548C\u6C38\u4E45 Sakupa \u5730\u5740\u672A\u53D8\u3002",
2271
+ summary: "Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged.",
2205
2272
  data: { result },
2206
2273
  nextActions: [{ tool: "site_status", allowed: true }]
2207
2274
  });
@@ -2234,7 +2301,7 @@ function registerBillingTools(server, ctx) {
2234
2301
  schemaVersion: 1,
2235
2302
  outcome: "completed",
2236
2303
  resultCode: "billing_catalog_returned",
2237
- summary: `\u5DF2\u8FD4\u56DE ${catalog.plans.length} \u4E2A\u6708\u4ED8\u65B9\u6848\uFF1BStripe \u6258\u7BA1\u9875\u9762\u662F\u4ED8\u8D39\u4E0E\u6539\u6863\u7684\u6700\u7EC8\u786E\u8BA4\u5165\u53E3\u3002`,
2304
+ summary: `Returned ${catalog.plans.length} monthly plans; the Stripe-hosted page is the final confirmation surface for payment and plan changes.`,
2238
2305
  data: { catalog },
2239
2306
  nextActions: [{ tool: "subscribe_site", allowed: true }]
2240
2307
  });
@@ -2267,13 +2334,13 @@ function registerBillingTools(server, ctx) {
2267
2334
  outcome: "waiting_user",
2268
2335
  resultCode: "stripe_plan_change_confirmation_required",
2269
2336
  operationId: args.operationId,
2270
- summary: `\u5DF2\u751F\u6210\u4ECE ${result.currentPlan} \u5230 ${result.targetPlan} \u7684 Stripe \u786E\u8BA4\u94FE\u63A5\uFF1B\u8BA2\u9605\u5C1A\u672A\u53D8\u66F4\u3002`,
2337
+ summary: `Stripe confirmation link created for ${result.currentPlan} -> ${result.targetPlan}; the subscription has NOT changed yet.`,
2271
2338
  data: { result },
2272
2339
  userAction: {
2273
2340
  type: "open_url",
2274
2341
  provider: "stripe",
2275
2342
  url: result.portalUrl,
2276
- expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u786E\u8BA4\u540E\uFF0C\u7531 webhook \u66F4\u65B0 Sakupa \u8BA2\u9605\u72B6\u6001"
2343
+ expectedOutcome: "After the user confirms on the Stripe-hosted page, the webhook updates the Sakupa subscription state"
2277
2344
  },
2278
2345
  nextActions: [{ tool: "billing_status", allowed: true }]
2279
2346
  });
@@ -2358,7 +2425,7 @@ export {
2358
2425
  createSakupaMcpServer,
2359
2426
  deleteSiteFile,
2360
2427
  loadMcpRuntimeConfig,
2361
- readSiteFile,
2428
+ loadSiteFile,
2362
2429
  registerLifecycleTools,
2363
2430
  registerTools,
2364
2431
  requireSiteFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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",