@sakupa/mcp 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +32 -64
  2. package/dist/index.js +32 -64
  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.5";
131
+ var SAKUPA_MCP_VERSION = "0.7.7";
132
132
 
133
133
  // ../core/dist/domain/errors.js
134
134
  var HTTP_STATUS = {
@@ -407,22 +407,13 @@ function validateDeployableFiles(files, opts) {
407
407
  severity: "warning",
408
408
  code: "missing_html_lang",
409
409
  path: entryHtmlPath,
410
- message: 'The entry HTML has no lang attribute. Add html lang="en" | "ja" | "zh-CN" so Sakupa surfaces match the site language.'
410
+ message: 'The entry HTML has no lang attribute. Before deploying, add one matching the content language (html lang="en" | "ja" | "zh-CN") \u2014 it drives screen-reader pronunciation and search-engine language detection, and Sakupa surfaces follow it.'
411
411
  });
412
412
  }
413
413
  }
414
414
  }
415
415
  const jsCount = files.filter((f) => ["js", "mjs"].includes(fileExtension(f.path))).length;
416
416
  const looksLikeSpa = htmlPaths.length === 1 && entryHtmlPath === "index.html" && jsCount > 0;
417
- const wantsSpa = opts.spaFallbackRequested === true || looksLikeSpa;
418
- const spaFallbackConfirmationRequired = wantsSpa && opts.spaFallbackConfirmed !== true;
419
- if (opts.spaFallbackRequested === true && opts.spaFallbackConfirmed !== true) {
420
- issues.push({
421
- severity: "error",
422
- code: "spa_fallback_confirmation_required",
423
- message: "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. It must be explicitly confirmed."
424
- });
425
- }
426
417
  const ok = issues.every((i) => i.severity !== "error");
427
418
  return {
428
419
  ok,
@@ -432,8 +423,7 @@ function validateDeployableFiles(files, opts) {
432
423
  entryHtmlPath,
433
424
  htmlLang,
434
425
  supportedLang,
435
- looksLikeSpa,
436
- spaFallbackConfirmationRequired
426
+ looksLikeSpa
437
427
  };
438
428
  }
439
429
  function safeDecode(bytes) {
@@ -1052,7 +1042,7 @@ async function analyzeProject(projectDir, opts = {}) {
1052
1042
  fileCount: 0,
1053
1043
  issues: [],
1054
1044
  ssrRisks,
1055
- spa: { looksLikeSpa: false, fallbackRecommended: false, confirmationRequired: false },
1045
+ spa: { looksLikeSpa: false, autoFallback: false },
1056
1046
  deployable: false,
1057
1047
  suggestedNextAction: suggestedNextAction2
1058
1048
  };
@@ -1072,30 +1062,23 @@ async function analyzeProject(projectDir, opts = {}) {
1072
1062
  }
1073
1063
  candidates.push({ path: file.path, size: file.size, ...content ? { content } : {} });
1074
1064
  }
1075
- const validation = validateDeployableFiles(candidates, {
1076
- mode: "free",
1077
- ...opts.spaFallbackRequested !== void 0 ? { spaFallbackRequested: opts.spaFallbackRequested } : {},
1078
- ...opts.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: opts.spaFallbackConfirmed } : {}
1079
- });
1065
+ const validation = validateDeployableFiles(candidates, { mode: "free" });
1080
1066
  ssrRisks.push(...serverAndDbDepRisks(pkg, true));
1081
1067
  const deployable = validation.ok && walked.length > 0;
1082
1068
  const spa = {
1083
1069
  looksLikeSpa: validation.looksLikeSpa,
1084
- fallbackRecommended: validation.looksLikeSpa,
1085
- confirmationRequired: validation.spaFallbackConfirmationRequired
1070
+ autoFallback: validation.looksLikeSpa
1086
1071
  };
1087
1072
  let suggestedNextAction;
1088
1073
  if (!deployable) {
1089
1074
  const firstError = validation.issues.find((i) => i.severity === "error");
1090
1075
  if (firstError?.code === "missing_index_html") {
1091
1076
  suggestedNextAction = `No index.html at the root of "${outputDirRel}". Deploy the built static output (the directory whose root contains index.html), not the source project. Build locally first if needed (${buildCommandHint ?? "npm run build"}), then re-run analyze_site.`;
1092
- } else if (firstError?.code === "spa_fallback_confirmation_required") {
1093
- suggestedNextAction = "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. Confirm it explicitly: re-run with spaFallbackRequested: true and spaFallbackConfirmed: true.";
1094
1077
  } else {
1095
1078
  suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
1096
1079
  }
1097
- } else if (spa.confirmationRequired) {
1098
- suggestedNextAction = `The output in "${outputDirRel}" is deployable, but it looks like a single-page app. Decide about SPA fallback first: run deploy_site with spaFallback: true and spaFallbackConfirmed: true to enable it, or with spaFallbackConfirmed: true alone to deploy without fallback.`;
1080
+ } else if (spa.looksLikeSpa) {
1081
+ suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}". It looks like a single-page app, so SPA fallback (unknown paths rewrite to index.html) will be enabled automatically; pass spaFallback: false to opt out.`;
1099
1082
  } else {
1100
1083
  suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
1101
1084
  }
@@ -1299,7 +1282,8 @@ function toolError(e) {
1299
1282
  ([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
1300
1283
  )
1301
1284
  ) : void 0;
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.";
1285
+ const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
1286
+ const safeSummary = errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "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.";
1303
1287
  const result = structuredToolResult({
1304
1288
  schemaVersion: 1,
1305
1289
  outcome: "failed",
@@ -1341,7 +1325,7 @@ ${JSON.stringify(obj, null, 2)}`;
1341
1325
  var planEnum = z2.enum(["water", "personal", "share", "business"]);
1342
1326
  var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
1343
1327
  function planCatalog() {
1344
- return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
1328
+ return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
1345
1329
  }
1346
1330
  var ticketCategoryEnum = z2.enum([
1347
1331
  "billing",
@@ -1369,22 +1353,6 @@ Analysis:`,
1369
1353
  "blocked"
1370
1354
  );
1371
1355
  }
1372
- function spaConfirmationResult(analysis) {
1373
- return text(
1374
- "spa_fallback_confirmation_required",
1375
- `SPA fallback confirmation required \u2014 nothing was deployed yet.
1376
-
1377
- This site looks like a single-page application (one index.html plus JavaScript). SPA fallback rewrites every unknown path to index.html so client-side routes work, but it CHANGES normal 404 behavior: visitors never see a not-found page.
1378
-
1379
- Please ask the user to choose, then re-run deploy_site with:
1380
- - spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
1381
- - spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
1382
-
1383
- Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
1384
- { analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
1385
- "waiting_user"
1386
- );
1387
- }
1388
1356
  var MB2 = 1024 * 1024;
1389
1357
  function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
1390
1358
  const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
@@ -1446,17 +1414,13 @@ function registerTools(server, ctx) {
1446
1414
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1447
1415
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1448
1416
  inputSchema: {
1449
- outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1450
- spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
1451
- spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
1417
+ outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
1452
1418
  }
1453
1419
  },
1454
1420
  async (args) => {
1455
1421
  try {
1456
1422
  const analysis = await analyzeProject(ctx.projectDir, {
1457
- ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {},
1458
- ...args.spaFallbackRequested !== void 0 ? { spaFallbackRequested: args.spaFallbackRequested } : {},
1459
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1423
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1460
1424
  });
1461
1425
  return textJson(
1462
1426
  "site_analysis_completed",
@@ -1477,8 +1441,9 @@ Next action: ${analysis.suggestedNextAction}`,
1477
1441
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1478
1442
  inputSchema: {
1479
1443
  outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1480
- spaFallback: z2.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
1481
- spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
1444
+ spaFallback: z2.boolean().optional().describe(
1445
+ "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."
1446
+ ),
1482
1447
  publicConfirmed: z2.boolean().optional().describe(
1483
1448
  "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
1484
1449
  ),
@@ -1488,13 +1453,8 @@ Next action: ${analysis.suggestedNextAction}`,
1488
1453
  async (args) => {
1489
1454
  try {
1490
1455
  const analysis = await analyzeProject(ctx.projectDir, {
1491
- ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {},
1492
- ...args.spaFallback !== void 0 ? { spaFallbackRequested: args.spaFallback } : {},
1493
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1456
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1494
1457
  });
1495
- if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
1496
- return spaConfirmationResult(analysis);
1497
- }
1498
1458
  if (!analysis.deployable || !analysis.files) {
1499
1459
  return notDeployableResult(analysis);
1500
1460
  }
@@ -1526,8 +1486,7 @@ Next action: ${analysis.suggestedNextAction}`,
1526
1486
  const created = await ctx.client.createSite({
1527
1487
  manifest,
1528
1488
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1529
- spaFallback: args.spaFallback === true,
1530
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1489
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
1531
1490
  });
1532
1491
  const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
1533
1492
  const finalized2 = await ctx.client.finalizeDeployment(
@@ -1571,8 +1530,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1571
1530
  const req = {
1572
1531
  manifest,
1573
1532
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1574
- spaFallback: args.spaFallback === true,
1575
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
1533
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
1576
1534
  ...forceFullUpload ? { forceFullUpload: true } : {}
1577
1535
  };
1578
1536
  const deployment = await ctx.client.createDeployment(
@@ -1647,7 +1605,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1647
1605
  return text(
1648
1606
  "site_refreshed",
1649
1607
  `Site validity refreshed. New expiry: ${res.expiresAt}
1650
- Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
1608
+ NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy_site. Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
1651
1609
  { siteId: site.siteId, expiresAt: res.expiresAt }
1652
1610
  );
1653
1611
  } catch (e) {
@@ -1698,7 +1656,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
1698
1656
  );
1699
1657
  return text(
1700
1658
  "subscription_checkout_ready",
1701
- `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
1659
+ `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
1702
1660
  ${res.checkoutUrl}
1703
1661
 
1704
1662
  Open this link in a browser to subscribe. Card data is entered only on the Stripe-hosted page \u2014 never give card numbers, passwords or security codes to the AI tool.
@@ -1810,7 +1768,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
1810
1768
  const lines = [
1811
1769
  `Billing status for site ${res.siteId} (mode: ${res.mode})`,
1812
1770
  res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
1813
- res.plan ? `Plan: ${res.plan} (\xA5${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1771
+ res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1814
1772
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
1815
1773
  res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
1816
1774
  res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
@@ -2393,6 +2351,16 @@ Safety boundaries:
2393
2351
  - Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
2394
2352
  - Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
2395
2353
  - A subscription never grants domain ownership; only DNS verification does.
2354
+ - Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
2355
+ into the conversation copies the site's only key outside the protected local file. Read it
2356
+ only through the tools.
2357
+ - Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
2358
+ language (infer it from the content) and then deploy; only skip when the user explicitly
2359
+ wants no lang attribute.
2360
+ - Prices are authoritative in JPY (Japanese yen). When talking with a user in a language
2361
+ other than Japanese, look up the approximate exchange rate and show an estimated local
2362
+ price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
2363
+ real charge in JPY. Never show a bare Yen sign.
2396
2364
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
2397
2365
  a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
2398
2366
  Stripe's public no-code portal login, where the customer verifies the checkout email with a
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.5";
126
+ var SAKUPA_MCP_VERSION = "0.7.7";
127
127
 
128
128
  // ../core/dist/domain/errors.js
129
129
  var HTTP_STATUS = {
@@ -402,22 +402,13 @@ function validateDeployableFiles(files, opts) {
402
402
  severity: "warning",
403
403
  code: "missing_html_lang",
404
404
  path: entryHtmlPath,
405
- message: 'The entry HTML has no lang attribute. Add html lang="en" | "ja" | "zh-CN" so Sakupa surfaces match the site language.'
405
+ message: 'The entry HTML has no lang attribute. Before deploying, add one matching the content language (html lang="en" | "ja" | "zh-CN") \u2014 it drives screen-reader pronunciation and search-engine language detection, and Sakupa surfaces follow it.'
406
406
  });
407
407
  }
408
408
  }
409
409
  }
410
410
  const jsCount = files.filter((f) => ["js", "mjs"].includes(fileExtension(f.path))).length;
411
411
  const looksLikeSpa = htmlPaths.length === 1 && entryHtmlPath === "index.html" && jsCount > 0;
412
- const wantsSpa = opts.spaFallbackRequested === true || looksLikeSpa;
413
- const spaFallbackConfirmationRequired = wantsSpa && opts.spaFallbackConfirmed !== true;
414
- if (opts.spaFallbackRequested === true && opts.spaFallbackConfirmed !== true) {
415
- issues.push({
416
- severity: "error",
417
- code: "spa_fallback_confirmation_required",
418
- message: "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. It must be explicitly confirmed."
419
- });
420
- }
421
412
  const ok = issues.every((i) => i.severity !== "error");
422
413
  return {
423
414
  ok,
@@ -427,8 +418,7 @@ function validateDeployableFiles(files, opts) {
427
418
  entryHtmlPath,
428
419
  htmlLang,
429
420
  supportedLang,
430
- looksLikeSpa,
431
- spaFallbackConfirmationRequired
421
+ looksLikeSpa
432
422
  };
433
423
  }
434
424
  function safeDecode(bytes) {
@@ -1215,7 +1205,7 @@ async function analyzeProject(projectDir, opts = {}) {
1215
1205
  fileCount: 0,
1216
1206
  issues: [],
1217
1207
  ssrRisks,
1218
- spa: { looksLikeSpa: false, fallbackRecommended: false, confirmationRequired: false },
1208
+ spa: { looksLikeSpa: false, autoFallback: false },
1219
1209
  deployable: false,
1220
1210
  suggestedNextAction: suggestedNextAction2
1221
1211
  };
@@ -1235,30 +1225,23 @@ async function analyzeProject(projectDir, opts = {}) {
1235
1225
  }
1236
1226
  candidates.push({ path: file.path, size: file.size, ...content ? { content } : {} });
1237
1227
  }
1238
- const validation = validateDeployableFiles(candidates, {
1239
- mode: "free",
1240
- ...opts.spaFallbackRequested !== void 0 ? { spaFallbackRequested: opts.spaFallbackRequested } : {},
1241
- ...opts.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: opts.spaFallbackConfirmed } : {}
1242
- });
1228
+ const validation = validateDeployableFiles(candidates, { mode: "free" });
1243
1229
  ssrRisks.push(...serverAndDbDepRisks(pkg, true));
1244
1230
  const deployable = validation.ok && walked.length > 0;
1245
1231
  const spa = {
1246
1232
  looksLikeSpa: validation.looksLikeSpa,
1247
- fallbackRecommended: validation.looksLikeSpa,
1248
- confirmationRequired: validation.spaFallbackConfirmationRequired
1233
+ autoFallback: validation.looksLikeSpa
1249
1234
  };
1250
1235
  let suggestedNextAction;
1251
1236
  if (!deployable) {
1252
1237
  const firstError = validation.issues.find((i) => i.severity === "error");
1253
1238
  if (firstError?.code === "missing_index_html") {
1254
1239
  suggestedNextAction = `No index.html at the root of "${outputDirRel}". Deploy the built static output (the directory whose root contains index.html), not the source project. Build locally first if needed (${buildCommandHint ?? "npm run build"}), then re-run analyze_site.`;
1255
- } else if (firstError?.code === "spa_fallback_confirmation_required") {
1256
- suggestedNextAction = "SPA fallback rewrites unknown paths to index.html and changes normal 404 behavior. Confirm it explicitly: re-run with spaFallbackRequested: true and spaFallbackConfirmed: true.";
1257
1240
  } else {
1258
1241
  suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
1259
1242
  }
1260
- } else if (spa.confirmationRequired) {
1261
- suggestedNextAction = `The output in "${outputDirRel}" is deployable, but it looks like a single-page app. Decide about SPA fallback first: run deploy_site with spaFallback: true and spaFallbackConfirmed: true to enable it, or with spaFallbackConfirmed: true alone to deploy without fallback.`;
1243
+ } else if (spa.looksLikeSpa) {
1244
+ suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}". It looks like a single-page app, so SPA fallback (unknown paths rewrite to index.html) will be enabled automatically; pass spaFallback: false to opt out.`;
1262
1245
  } else {
1263
1246
  suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
1264
1247
  }
@@ -1357,7 +1340,8 @@ function toolError(e) {
1357
1340
  ([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
1358
1341
  )
1359
1342
  ) : void 0;
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.";
1343
+ const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
1344
+ const safeSummary = errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "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.";
1361
1345
  const result = structuredToolResult({
1362
1346
  schemaVersion: 1,
1363
1347
  outcome: "failed",
@@ -1403,7 +1387,7 @@ ${JSON.stringify(obj, null, 2)}`;
1403
1387
  var planEnum = z2.enum(["water", "personal", "share", "business"]);
1404
1388
  var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
1405
1389
  function planCatalog() {
1406
- return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
1390
+ return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
1407
1391
  }
1408
1392
  var ticketCategoryEnum = z2.enum([
1409
1393
  "billing",
@@ -1431,22 +1415,6 @@ Analysis:`,
1431
1415
  "blocked"
1432
1416
  );
1433
1417
  }
1434
- function spaConfirmationResult(analysis) {
1435
- return text(
1436
- "spa_fallback_confirmation_required",
1437
- `SPA fallback confirmation required \u2014 nothing was deployed yet.
1438
-
1439
- This site looks like a single-page application (one index.html plus JavaScript). SPA fallback rewrites every unknown path to index.html so client-side routes work, but it CHANGES normal 404 behavior: visitors never see a not-found page.
1440
-
1441
- Please ask the user to choose, then re-run deploy_site with:
1442
- - spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
1443
- - spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
1444
-
1445
- Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
1446
- { analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
1447
- "waiting_user"
1448
- );
1449
- }
1450
1418
  var MB2 = 1024 * 1024;
1451
1419
  function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
1452
1420
  const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
@@ -1508,17 +1476,13 @@ function registerTools(server, ctx) {
1508
1476
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1509
1477
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1510
1478
  inputSchema: {
1511
- outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1512
- spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
1513
- spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
1479
+ outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
1514
1480
  }
1515
1481
  },
1516
1482
  async (args) => {
1517
1483
  try {
1518
1484
  const analysis = await analyzeProject(ctx.projectDir, {
1519
- ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {},
1520
- ...args.spaFallbackRequested !== void 0 ? { spaFallbackRequested: args.spaFallbackRequested } : {},
1521
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1485
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1522
1486
  });
1523
1487
  return textJson(
1524
1488
  "site_analysis_completed",
@@ -1539,8 +1503,9 @@ Next action: ${analysis.suggestedNextAction}`,
1539
1503
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1540
1504
  inputSchema: {
1541
1505
  outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1542
- spaFallback: z2.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
1543
- spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
1506
+ spaFallback: z2.boolean().optional().describe(
1507
+ "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."
1508
+ ),
1544
1509
  publicConfirmed: z2.boolean().optional().describe(
1545
1510
  "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
1546
1511
  ),
@@ -1550,13 +1515,8 @@ Next action: ${analysis.suggestedNextAction}`,
1550
1515
  async (args) => {
1551
1516
  try {
1552
1517
  const analysis = await analyzeProject(ctx.projectDir, {
1553
- ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {},
1554
- ...args.spaFallback !== void 0 ? { spaFallbackRequested: args.spaFallback } : {},
1555
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1518
+ ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
1556
1519
  });
1557
- if (analysis.spa.confirmationRequired && args.spaFallbackConfirmed !== true) {
1558
- return spaConfirmationResult(analysis);
1559
- }
1560
1520
  if (!analysis.deployable || !analysis.files) {
1561
1521
  return notDeployableResult(analysis);
1562
1522
  }
@@ -1588,8 +1548,7 @@ Next action: ${analysis.suggestedNextAction}`,
1588
1548
  const created = await ctx.client.createSite({
1589
1549
  manifest,
1590
1550
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1591
- spaFallback: args.spaFallback === true,
1592
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
1551
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {}
1593
1552
  });
1594
1553
  const uploaded2 = await uploadAll(ctx, created.uploadTargets, files, outputAbs);
1595
1554
  const finalized2 = await ctx.client.finalizeDeployment(
@@ -1633,8 +1592,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1633
1592
  const req = {
1634
1593
  manifest,
1635
1594
  ...args.lang !== void 0 ? { lang: args.lang } : {},
1636
- spaFallback: args.spaFallback === true,
1637
- ...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
1595
+ ...args.spaFallback !== void 0 ? { spaFallback: args.spaFallback } : {},
1638
1596
  ...forceFullUpload ? { forceFullUpload: true } : {}
1639
1597
  };
1640
1598
  const deployment = await ctx.client.createDeployment(
@@ -1709,7 +1667,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1709
1667
  return text(
1710
1668
  "site_refreshed",
1711
1669
  `Site validity refreshed. New expiry: ${res.expiresAt}
1712
- Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
1670
+ NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy_site. Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
1713
1671
  { siteId: site.siteId, expiresAt: res.expiresAt }
1714
1672
  );
1715
1673
  } catch (e) {
@@ -1760,7 +1718,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
1760
1718
  );
1761
1719
  return text(
1762
1720
  "subscription_checkout_ready",
1763
- `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
1721
+ `Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, JPY ${res.monthlyPriceJpy}/month (Japanese yen)
1764
1722
  ${res.checkoutUrl}
1765
1723
 
1766
1724
  Open this link in a browser to subscribe. Card data is entered only on the Stripe-hosted page \u2014 never give card numbers, passwords or security codes to the AI tool.
@@ -1872,7 +1830,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
1872
1830
  const lines = [
1873
1831
  `Billing status for site ${res.siteId} (mode: ${res.mode})`,
1874
1832
  res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
1875
- res.plan ? `Plan: ${res.plan} (\xA5${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1833
+ res.plan ? `Plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
1876
1834
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
1877
1835
  res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
1878
1836
  res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
@@ -2386,6 +2344,16 @@ Safety boundaries:
2386
2344
  - Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
2387
2345
  - Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
2388
2346
  - A subscription never grants domain ownership; only DNS verification does.
2347
+ - Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
2348
+ into the conversation copies the site's only key outside the protected local file. Read it
2349
+ only through the tools.
2350
+ - Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
2351
+ language (infer it from the content) and then deploy; only skip when the user explicitly
2352
+ wants no lang attribute.
2353
+ - Prices are authoritative in JPY (Japanese yen). When talking with a user in a language
2354
+ other than Japanese, look up the approximate exchange rate and show an estimated local
2355
+ price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
2356
+ real charge in JPY. Never show a bare Yen sign.
2389
2357
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
2390
2358
  a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
2391
2359
  Stripe's public no-code portal login, where the customer verifies the checkout email with a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
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",