@sakupa/mcp 0.7.13 → 0.7.14

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 +111 -99
  2. package/dist/index.js +111 -99
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
129
129
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
130
130
 
131
131
  // ../core/dist/domain/version.js
132
- var SAKUPA_MCP_VERSION = "0.7.13";
132
+ var SAKUPA_MCP_VERSION = "0.7.14";
133
133
 
134
134
  // ../core/dist/domain/errors.js
135
135
  var HTTP_STATUS = {
@@ -1029,13 +1029,13 @@ async function analyzeProject(projectDir, opts = {}) {
1029
1029
  const sourceWithoutBuild = pkg !== null && buildRequired && (await isDirectory(join(root, "src")) || await isDirectory(join(root, "pages")));
1030
1030
  let suggestedNextAction2;
1031
1031
  if (opts.outputDir !== void 0) {
1032
- suggestedNextAction2 = `The requested output directory "${opts.outputDir}" does not exist. Build the project locally first (${buildCommandHint ?? "npm run build"}) or pass the correct directory, then re-run analyze_site.`;
1032
+ suggestedNextAction2 = `The requested output directory "${opts.outputDir}" does not exist. Build the project locally first (${buildCommandHint ?? "npm run build"}) or pass the correct directory, then re-run analyze.`;
1033
1033
  } else if (ssrRisks.length > 0 && detection) {
1034
- suggestedNextAction2 = `This ${detection.framework} project appears to require a server runtime (see ssrRisks) and no static output directory was found. Convert it to static output (e.g. Next.js output: 'export', Nuxt generate, Astro static, SvelteKit adapter-static), run the build locally (${buildCommandHint ?? "npm run build"}), then re-run analyze_site.`;
1034
+ suggestedNextAction2 = `This ${detection.framework} project appears to require a server runtime (see ssrRisks) and no static output directory was found. Convert it to static output (e.g. Next.js output: 'export', Nuxt generate, Astro static, SvelteKit adapter-static), run the build locally (${buildCommandHint ?? "npm run build"}), then re-run analyze.`;
1035
1035
  } else if (sourceWithoutBuild || detection && !outputDirExists) {
1036
- suggestedNextAction2 = `This looks like a source project, not built static output. Run the build locally (${buildCommandHint ?? "npm run build"}) then re-run analyze_site.`;
1036
+ suggestedNextAction2 = `This looks like a source project, not built static output. Run the build locally (${buildCommandHint ?? "npm run build"}) then re-run analyze.`;
1037
1037
  } else {
1038
- suggestedNextAction2 = "No deployable static output was found. Create an index.html (or build the project locally so a static output directory exists), then re-run analyze_site.";
1038
+ suggestedNextAction2 = "No deployable static output was found. Create an index.html (or build the project locally so a static output directory exists), then re-run analyze.";
1039
1039
  }
1040
1040
  return {
1041
1041
  projectType,
@@ -1080,14 +1080,14 @@ async function analyzeProject(projectDir, opts = {}) {
1080
1080
  if (!deployable) {
1081
1081
  const firstError = validation.issues.find((i) => i.severity === "error");
1082
1082
  if (firstError?.code === "missing_index_html") {
1083
- 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.`;
1083
+ 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.`;
1084
1084
  } else {
1085
- suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
1085
+ suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze.";
1086
1086
  }
1087
1087
  } else if (spa.looksLikeSpa) {
1088
- 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.`;
1088
+ suggestedNextAction = `Run deploy 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.`;
1089
1089
  } else {
1090
- suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
1090
+ suggestedNextAction = `Run deploy to publish the static output in "${outputDirRel}".`;
1091
1091
  }
1092
1092
  return {
1093
1093
  projectType,
@@ -1242,13 +1242,15 @@ function writeAll(records) {
1242
1242
  writeFileSync2(path, `${JSON.stringify(records, null, 2)}
1243
1243
  `, "utf-8");
1244
1244
  }
1245
- function listRecentCreations(nowMs) {
1245
+ function listRecentCreations(nowMs, apiBaseUrl) {
1246
1246
  return readAll().filter((e) => {
1247
1247
  const t = Date.parse(e.createdAt);
1248
- return Number.isFinite(t) && nowMs - t < RECENT_WINDOW_MS;
1248
+ if (!Number.isFinite(t) || nowMs - t >= RECENT_WINDOW_MS) return false;
1249
+ return e.apiBaseUrl === void 0 || e.apiBaseUrl === apiBaseUrl;
1249
1250
  });
1250
1251
  }
1251
1252
  function recordCreation(record) {
1253
+ knownQuotaFree.delete(record.siteId);
1252
1254
  const rest = readAll().filter((e) => e.siteId !== record.siteId);
1253
1255
  writeAll([...rest, record]);
1254
1256
  }
@@ -1257,6 +1259,12 @@ function removeCreation(siteId) {
1257
1259
  const rest = all.filter((e) => e.siteId !== siteId);
1258
1260
  if (rest.length !== all.length) writeAll(rest);
1259
1261
  }
1262
+ var knownQuotaFree = /* @__PURE__ */ new Set();
1263
+ function noteSiteMode(siteId, mode) {
1264
+ if (mode !== "paid" || knownQuotaFree.has(siteId)) return;
1265
+ removeCreation(siteId);
1266
+ knownQuotaFree.add(siteId);
1267
+ }
1260
1268
 
1261
1269
  // src/dns-doh.ts
1262
1270
  var dohFetch = (input, init) => fetch(input, init);
@@ -1444,7 +1452,10 @@ var projectDirInput = z2.string().optional().describe(
1444
1452
  "Absolute path of the user's PROJECT ROOT \u2014 the folder the user opened/works in (for framework projects: where package.json lives, NEVER the build-output subfolder like dist/out; the analyzer locates the output automatically). .sakupa/site.json lives here, so PASS THE SAME DIRECTORY EVERY TIME for the same project. When omitted the server falls back to its startup directory, which may not be where the user is working now."
1445
1453
  );
1446
1454
  function withProjectDir(ctx, projectDirArg) {
1447
- if (projectDirArg === void 0) return ctx;
1455
+ if (projectDirArg === void 0) {
1456
+ const sticky = ctx.session.projectDir;
1457
+ return sticky !== null ? { ...ctx, projectDir: sticky } : ctx;
1458
+ }
1448
1459
  if (!isAbsolute(projectDirArg)) {
1449
1460
  throw new LocalGuidanceError(
1450
1461
  "invalid_request",
@@ -1465,6 +1476,7 @@ function withProjectDir(ctx, projectDirArg) {
1465
1476
  `projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
1466
1477
  );
1467
1478
  }
1479
+ ctx.session.projectDir = dir;
1468
1480
  return { ...ctx, projectDir: dir };
1469
1481
  }
1470
1482
  function requireSiteFile(ctx) {
@@ -1478,7 +1490,7 @@ function requireSiteFile(ctx) {
1478
1490
  if (state.kind === "absent") {
1479
1491
  throw new LocalGuidanceError(
1480
1492
  "not_found",
1481
- `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.`
1493
+ `No .sakupa/site.json found in ${ctx.projectDir} \u2014 this directory has no Sakupa site binding. If you meant to manage (delete/status/bind) an EXISTING site, re-run this tool with projectDir set to THAT site's own directory (each site's binding lives in its own folder). To publish THIS directory as a new site, run deploy. If this was a paid custom-domain site whose project file was lost, use recover.`
1482
1494
  );
1483
1495
  }
1484
1496
  return state.file;
@@ -1618,7 +1630,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
1618
1630
  if (bytes.byteLength !== match.size) {
1619
1631
  throw new SakupaError(
1620
1632
  "validation_failed",
1621
- `"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy_site so Sakupa re-checks the current files before uploading.`
1633
+ `"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy so Sakupa re-checks the current files before uploading.`
1622
1634
  );
1623
1635
  }
1624
1636
  await ctx.client.uploadFile(target, bytes);
@@ -1633,7 +1645,7 @@ Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provision
1633
1645
  check = await client.checkVerification(pb.verificationId, credential);
1634
1646
  } catch {
1635
1647
  return {
1636
- note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try site_status again shortly.)"
1648
+ note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try status again shortly.)"
1637
1649
  };
1638
1650
  }
1639
1651
  try {
@@ -1653,7 +1665,7 @@ Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provision
1653
1665
  ${block}`, checklist: toDnsChecklist(diag.checks) };
1654
1666
  } catch {
1655
1667
  return {
1656
- note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind_domain with action "status" for the per-record checklist.)'
1668
+ note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind with action "status" for the per-record checklist.)'
1657
1669
  };
1658
1670
  }
1659
1671
  }
@@ -1671,17 +1683,19 @@ function findNeighborBinding(projectDir, outputRel) {
1671
1683
  }
1672
1684
  return null;
1673
1685
  }
1674
- function freeSiteCreationBarrier() {
1675
- const recent = listRecentCreations(Date.now());
1686
+ function freeSiteCreationBarrier(apiBaseUrl) {
1687
+ const recent = listRecentCreations(Date.now(), apiBaseUrl);
1676
1688
  if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
1677
1689
  const registryPath = creationRegistryPath();
1678
1690
  return text(
1679
1691
  "local_site_limit_reached",
1680
- `This machine already created ${recent.length} free sites in the last 24 hours (the server also enforces ${FREE_ACTIVE_SITES_PER_IP} active free sites per IP). No new site was created.
1692
+ `LOCAL PRECHECK by this MCP client (its own creation registry \u2014 the server was NOT contacted): this machine already created ${recent.length} sites in this environment in the last 24 hours, matching the server's limit of ${FREE_ACTIVE_SITES_PER_IP} active free sites per IP. No new site was created.
1681
1693
 
1682
1694
  ` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
1683
1695
 
1684
- Options: delete one of these sites (run delete_site with its projectDir), wait for a free site to expire, or \u2014 if this list is stale because sites were deleted or subscribed elsewhere \u2014 remove the local registry file at ${registryPath} and retry.`,
1696
+ How a slot frees up: (1) delete one of the sites above \u2014 run delete with that site's projectDir; its slot frees immediately; (2) every record expires on its own 24 hours after creation; (3) a site that upgrades to a paid plan stops counting the next time any tool sees it. Deleting a project's .sakupa folder does NOT free a slot: this registry lives in the home directory and the server still counts the live site.
1697
+
1698
+ If this list is stale (sites deleted or subscribed from another machine), remove the local registry file at ${registryPath} and retry \u2014 that only skips this local precheck; the server still enforces the same per-IP limit and is the final authority.`,
1685
1699
  { recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
1686
1700
  "blocked"
1687
1701
  );
@@ -1689,9 +1703,9 @@ Options: delete one of these sites (run delete_site with its projectDir), wait f
1689
1703
  function registerTools(server, baseCtx) {
1690
1704
  const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
1691
1705
  server.registerTool(
1692
- "analyze_site",
1706
+ "analyze",
1693
1707
  {
1694
- description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy_site.",
1708
+ description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy.",
1695
1709
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1696
1710
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1697
1711
  inputSchema: {
@@ -1717,9 +1731,9 @@ Next action: ${analysis.suggestedNextAction}`,
1717
1731
  }
1718
1732
  );
1719
1733
  server.registerTool(
1720
- "deploy_site",
1734
+ "deploy",
1721
1735
  {
1722
- description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze_site first and refuses to upload source projects, secrets, .env files, archives, media or server code. Never uploads anything when the analysis says the project is not deployable.`,
1736
+ description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. Never uploads anything when the analysis says the project is not deployable.`,
1723
1737
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1724
1738
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1725
1739
  inputSchema: {
@@ -1766,7 +1780,7 @@ Next action: ${analysis.suggestedNextAction}`,
1766
1780
  if (rootAbove) {
1767
1781
  return text(
1768
1782
  "not_project_root",
1769
- `${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy_site with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
1783
+ `${ctx.projectDir} has no package.json but ${rootAbove} does \u2014 this directory is a SUBFOLDER of that project (typically its build output), and .sakupa must live at the project ROOT. Re-run deploy with projectDir: ${rootAbove}. Only if the user explicitly says this subfolder is an INDEPENDENT site (e.g. a docs/ site inside a repo), re-run with subprojectConfirmed: true. Nothing was deployed and no site was created.`,
1770
1784
  { projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
1771
1785
  "blocked"
1772
1786
  );
@@ -1775,17 +1789,17 @@ Next action: ${analysis.suggestedNextAction}`,
1775
1789
  if (neighbor) {
1776
1790
  return text(
1777
1791
  "neighbor_binding_found",
1778
- `No .sakupa binding in ${ctx.projectDir}, but one EXISTS at ${neighbor} \u2014 this looks like the same project addressed at a different directory level. To update that existing site, re-run deploy_site with projectDir: ${neighbor}. Only if the user explicitly wants a SEPARATE new site, move this deploy to a directory outside that project. Nothing was deployed and no site was created.`,
1792
+ `No .sakupa binding in ${ctx.projectDir}, but one EXISTS at ${neighbor} \u2014 this looks like the same project addressed at a different directory level. To update that existing site, re-run deploy with projectDir: ${neighbor}. Only if the user explicitly wants a SEPARATE new site, move this deploy to a directory outside that project. Nothing was deployed and no site was created.`,
1779
1793
  { neighborProjectDir: neighbor },
1780
1794
  "blocked"
1781
1795
  );
1782
1796
  }
1783
- const barrier = freeSiteCreationBarrier();
1797
+ const barrier = freeSiteCreationBarrier(ctx.apiBaseUrl);
1784
1798
  if (barrier) return barrier;
1785
1799
  if (args.publicConfirmed !== true) {
1786
1800
  return text(
1787
1801
  "public_deployment_confirmation_required",
1788
- `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`,
1802
+ `First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy with publicConfirmed: true.`,
1789
1803
  { publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
1790
1804
  "waiting_user"
1791
1805
  );
@@ -1817,7 +1831,8 @@ Next action: ${analysis.suggestedNextAction}`,
1817
1831
  siteId: created.siteId,
1818
1832
  projectDir: ctx.projectDir,
1819
1833
  url: finalized2.url,
1820
- createdAt
1834
+ createdAt,
1835
+ apiBaseUrl: ctx.apiBaseUrl
1821
1836
  });
1822
1837
  return text(
1823
1838
  "site_published",
@@ -1827,7 +1842,7 @@ Project directory: ${ctx.projectDir}
1827
1842
  Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
1828
1843
  ` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
1829
1844
  ` : "") + `
1830
- This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh_site extends the validity; subscribing the site (subscribe_site) makes this URL permanent. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
1845
+ This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; subscribing the site (subscribe) makes this URL permanent. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
1831
1846
  ` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
1832
1847
  Warnings:
1833
1848
  ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
@@ -1886,6 +1901,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1886
1901
  }
1887
1902
  const { uploaded, finalized } = update;
1888
1903
  writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
1904
+ noteSiteMode(existing.siteId, finalized.mode);
1889
1905
  return text(
1890
1906
  "site_updated",
1891
1907
  `Site updated: ${finalized.url}
@@ -1894,7 +1910,7 @@ Project directory: ${ctx.projectDir}
1894
1910
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
1895
1911
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
1896
1912
  ` : "") + (finalized.mode === "free" ? `
1897
- Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh_site call. Subscribing (subscribe_site) makes the site permanent.
1913
+ Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
1898
1914
  ` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
1899
1915
  Warnings:
1900
1916
  ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
@@ -1916,7 +1932,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1916
1932
  }
1917
1933
  );
1918
1934
  server.registerTool(
1919
- "refresh_site",
1935
+ "refresh",
1920
1936
  {
1921
1937
  description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscribed sites are permanent and need no refresh.",
1922
1938
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -1931,7 +1947,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1931
1947
  return text(
1932
1948
  "site_refreshed",
1933
1949
  `Site validity refreshed (project: ${ctx.projectDir}). New expiry: ${res.expiresAt}
1934
- 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.`,
1950
+ NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy. Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
1935
1951
  { siteId: site.siteId, expiresAt: res.expiresAt, projectDir: ctx.projectDir }
1936
1952
  );
1937
1953
  } catch (e) {
@@ -1940,7 +1956,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
1940
1956
  }
1941
1957
  );
1942
1958
  server.registerTool(
1943
- "site_status",
1959
+ "status",
1944
1960
  {
1945
1961
  description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, subscription state, custom domains, size, last deployment and warnings.",
1946
1962
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -1952,8 +1968,9 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
1952
1968
  const ctx = withProjectDir(baseCtx, args.projectDir);
1953
1969
  const site = requireSiteFile(ctx);
1954
1970
  const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
1971
+ noteSiteMode(res.siteId, res.mode);
1955
1972
  const binding = res.pendingDomainBinding ? await describePendingBinding(ctx.client, site.credential, res.pendingDomainBinding) : void 0;
1956
- return textJson("site_status_returned", `Site status:${binding?.note ?? ""}`, {
1973
+ return textJson("status_returned", `Site status:${binding?.note ?? ""}`, {
1957
1974
  ...res,
1958
1975
  projectDir: ctx.projectDir,
1959
1976
  ...binding?.checklist ? { dnsChecklist: binding.checklist } : {}
@@ -1964,9 +1981,9 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
1964
1981
  }
1965
1982
  );
1966
1983
  server.registerTool(
1967
- "subscribe_site",
1984
+ "subscribe",
1968
1985
  {
1969
- description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). Paying makes the site PERMANENT on its ${previewHostPattern} URL \u2014 no more 24h expiry; that is the core value of paying. Binding a custom domain afterwards (bind_domain) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy_site first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
1986
+ description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). Paying makes the site PERMANENT on its ${previewHostPattern} URL \u2014 no more 24h expiry; that is the core value of paying. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
1970
1987
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1971
1988
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1972
1989
  inputSchema: {
@@ -1994,7 +2011,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
1994
2011
  ${res.checkoutUrl}
1995
2012
 
1996
2013
  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.
1997
- Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind_domain) is optional and still requires DNS verification.`,
2014
+ Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind) is optional and still requires DNS verification.`,
1998
2015
  {
1999
2016
  siteId: res.siteId,
2000
2017
  plan: res.plan,
@@ -2011,9 +2028,9 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2011
2028
  }
2012
2029
  );
2013
2030
  server.registerTool(
2014
- "bind_domain",
2031
+ "bind",
2015
2032
  {
2016
- description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
2033
+ description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
2017
2034
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2018
2035
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
2019
2036
  inputSchema: {
@@ -2051,7 +2068,7 @@ ${res2.message}
2051
2068
 
2052
2069
  ` + renderChecklistBlock(
2053
2070
  diag,
2054
- "Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status; if still failing after the attempts below, show the user this checklist."
2071
+ "Fix any [MISSING]/[FIX] lines above, then re-run bind status; if still failing after the attempts below, show the user this checklist."
2055
2072
  ),
2056
2073
  {
2057
2074
  verificationId: res2.verificationId,
@@ -2092,9 +2109,9 @@ Host fields above are the SHORT form: most DNS panels append the domain automati
2092
2109
 
2093
2110
  Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2094
2111
 
2095
- STEP 2 (after ownership verifies): Cloudflare issues certificate-validation TXT records \u2014 the verification result and bind_domain "status" list them the moment they exist; relay each to the user and add them too. Everything then completes automatically.
2112
+ STEP 2 (after ownership verifies): Cloudflare issues certificate-validation TXT records \u2014 the verification result and bind "status" list them the moment they exist; relay each to the user and add them too. Everything then completes automatically.
2096
2113
 
2097
- Drive the whole flow with bind_domain "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
2114
+ Drive the whole flow with bind "status": it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
2098
2115
  {
2099
2116
  verificationId: res.verificationId,
2100
2117
  apexDomain: apex,
@@ -2123,7 +2140,7 @@ Drive the whole flow with bind_domain "status": it live-checks every record and
2123
2140
  }
2124
2141
  );
2125
2142
  server.registerTool(
2126
- "billing_status",
2143
+ "billing",
2127
2144
  {
2128
2145
  description: "Show this site's hosting subscription: plan, payment state, current paid period, reconciled usage, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
2129
2146
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2135,6 +2152,7 @@ Drive the whole flow with bind_domain "status": it live-checks every record and
2135
2152
  const ctx = withProjectDir(baseCtx, args.projectDir);
2136
2153
  const site = requireSiteFile(ctx);
2137
2154
  const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
2155
+ noteSiteMode(res.siteId, res.mode);
2138
2156
  const lines = [
2139
2157
  `Billing status for site ${res.siteId} (mode: ${res.mode})`,
2140
2158
  res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
@@ -2145,9 +2163,9 @@ Drive the whole flow with bind_domain "status": it live-checks every record and
2145
2163
  res.estimatedUsageTier ? `Estimated usage tier: ${res.estimatedUsageTier}` : void 0,
2146
2164
  res.lastReconciledAt ? `Last usage reconciliation: ${res.lastReconciledAt}` : void 0,
2147
2165
  res.boundHostnames.length > 0 ? `Bound custom domains: ${res.boundHostnames.join(", ")}` : void 0,
2148
- res.risks.pastDue ? "ATTENTION: renewal payment failing \u2014 update the payment method (manage_billing). Serving continues while Stripe retries; if Stripe gives up, the site reverts to free." : void 0
2166
+ res.risks.pastDue ? "ATTENTION: renewal payment failing \u2014 update the payment method (portal). Serving continues while Stripe retries; if Stripe gives up, the site reverts to free." : void 0
2149
2167
  ].filter((l) => l !== void 0);
2150
- return textJson("billing_status_returned", `${lines.join("\n")}
2168
+ return textJson("billing_returned", `${lines.join("\n")}
2151
2169
 
2152
2170
  Full status:`, res);
2153
2171
  } catch (e) {
@@ -2156,7 +2174,7 @@ Full status:`, res);
2156
2174
  }
2157
2175
  );
2158
2176
  server.registerTool(
2159
- "manage_billing",
2177
+ "portal",
2160
2178
  {
2161
2179
  description: "Open the Stripe-hosted billing portal for this site: update the payment method, view invoices, or cancel the subscription. All billing operations happen on the Stripe-hosted page \u2014 never inside the AI tool. With .sakupa/site.json, this opens the site-specific portal. Without the local credential, this returns Stripe's public no-code Customer Portal login page. The customer enters the checkout email and confirms a one-time passcode sent by Stripe. This never restores Sakupa site authority.",
2162
2180
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2184,7 +2202,7 @@ Full status:`, res);
2184
2202
  url: res2.portalUrl,
2185
2203
  expectedOutcome: "The user manages payment methods, invoices, or cancels renewal on the Stripe-hosted page"
2186
2204
  },
2187
- nextActions: [{ tool: "billing_status", allowed: true }]
2205
+ nextActions: [{ tool: "billing", allowed: true }]
2188
2206
  });
2189
2207
  }
2190
2208
  const res = await ctx.client.getPublicBillingPortal();
@@ -2213,7 +2231,7 @@ Full status:`, res);
2213
2231
  }
2214
2232
  );
2215
2233
  server.registerTool(
2216
- "recover_domain_site",
2234
+ "recover",
2217
2235
  {
2218
2236
  description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Call first with the hostname to get the DNS record, then again with verificationId to complete recovery (writes a new .sakupa/site.json and returns a download link for the current site content).",
2219
2237
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2247,7 +2265,7 @@ ${res2.message}
2247
2265
 
2248
2266
  IMPORTANT: completing recovery revokes ALL previous local authorizations for this site by default (this protects you if the old project or its credential leaked). If you want to keep the old credentials working, pass preserveExistingCredentials: true when completing.
2249
2267
 
2250
- After the DNS record resolves, re-run recover_domain_site with verificationId: "${res2.verificationId}".`,
2268
+ After the DNS record resolves, re-run recover with verificationId: "${res2.verificationId}".`,
2251
2269
  {
2252
2270
  verificationId: res2.verificationId,
2253
2271
  apexDomain: res2.apexDomain,
@@ -2273,7 +2291,7 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
2273
2291
  data: { recovery: res2 },
2274
2292
  nextActions: [
2275
2293
  {
2276
- tool: "recover_domain_site",
2294
+ tool: "recover",
2277
2295
  arguments: { action: "complete", verificationId: args.verificationId },
2278
2296
  allowed: res2.readyToComplete,
2279
2297
  ...res2.readyToComplete ? {} : { reasonCode: res2.status }
@@ -2320,7 +2338,7 @@ ${res.archiveUrl}`,
2320
2338
  }
2321
2339
  );
2322
2340
  server.registerTool(
2323
- "create_support_ticket",
2341
+ "support",
2324
2342
  {
2325
2343
  description: "Create a Sakupa support ticket for billing, payment, refund review, domain verification, deployment, serving or other issues the MCP cannot solve automatically. Do not include secrets, credentials or card data in the description.",
2326
2344
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2355,14 +2373,14 @@ ${res.archiveUrl}`,
2355
2373
  }
2356
2374
  );
2357
2375
  server.registerTool(
2358
- "report_bug",
2376
+ "report",
2359
2377
  {
2360
2378
  description: "Prepare and submit a sanitized bug report when a Sakupa tool failed and the issue looks like a product bug. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
2361
2379
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2362
2380
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
2363
2381
  inputSchema: {
2364
2382
  projectDir: projectDirInput,
2365
- toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
2383
+ toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
2366
2384
  errorCode: z3.string().optional(),
2367
2385
  errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
2368
2386
  requestId: z3.string().optional(),
@@ -2407,7 +2425,7 @@ ${res.archiveUrl}`,
2407
2425
  const contactNote = args.contactEmail !== void 0 ? "note that their contact email is attached for follow-up. " : "ASK THEM ONCE whether they want to attach a contact email for follow-up (optional \u2014 omit if declined; include it as contactEmail when they do). ";
2408
2426
  return textJson(
2409
2427
  "bug_report_preview_ready",
2410
- `Bug report prepared but NOT submitted. This is the exact payload that would be sent (structured diagnostics only \u2014 no file contents, source code or secrets). Show it to the user, and ${contactNote}Then re-run report_bug with confirmSubmit: true to submit.`,
2428
+ `Bug report prepared but NOT submitted. This is the exact payload that would be sent (structured diagnostics only \u2014 no file contents, source code or secrets). Show it to the user, and ${contactNote}Then re-run report with confirmSubmit: true to submit.`,
2411
2429
  payload,
2412
2430
  "preview"
2413
2431
  );
@@ -2431,7 +2449,7 @@ import { z as z4 } from "zod";
2431
2449
  var plan = z4.enum(["water", "personal", "share", "business"]);
2432
2450
  function registerBillingTools(server, baseCtx) {
2433
2451
  server.registerTool(
2434
- "list_billing_plans",
2452
+ "plans",
2435
2453
  {
2436
2454
  description: "Return the authoritative Sakupa monthly plan catalog, exact limits, prices, catalog version and plan-change billing rules. This is read-only and does not require a site.",
2437
2455
  inputSchema: { projectDir: projectDirInput },
@@ -2448,7 +2466,7 @@ function registerBillingTools(server, baseCtx) {
2448
2466
  resultCode: "billing_catalog_returned",
2449
2467
  summary: `Returned ${catalog.plans.length} monthly plans; the Stripe-hosted page is the final confirmation surface for payment and plan changes.`,
2450
2468
  data: { catalog },
2451
- nextActions: [{ tool: "subscribe_site", allowed: true }]
2469
+ nextActions: [{ tool: "subscribe", allowed: true }]
2452
2470
  });
2453
2471
  } catch (error) {
2454
2472
  return toolError(error);
@@ -2456,7 +2474,7 @@ function registerBillingTools(server, baseCtx) {
2456
2474
  }
2457
2475
  );
2458
2476
  server.registerTool(
2459
- "change_subscription_plan",
2477
+ "upgrade",
2460
2478
  {
2461
2479
  description: "Create a Stripe-hosted confirmation link for a manually selected subscription plan. Creating the link does not change billing; only the user can confirm on Stripe.",
2462
2480
  inputSchema: {
@@ -2489,7 +2507,7 @@ function registerBillingTools(server, baseCtx) {
2489
2507
  url: result.portalUrl,
2490
2508
  expectedOutcome: "After the user confirms on the Stripe-hosted page, the webhook updates the Sakupa subscription state"
2491
2509
  },
2492
- nextActions: [{ tool: "billing_status", allowed: true }]
2510
+ nextActions: [{ tool: "billing", allowed: true }]
2493
2511
  });
2494
2512
  } catch (error) {
2495
2513
  return toolError(error);
@@ -2513,7 +2531,7 @@ var deleteConfirmation = z5.object({
2513
2531
  expectedCurrentPeriodEnd: z5.string().datetime().optional(),
2514
2532
  expectedLastDeploymentId: z5.string().optional(),
2515
2533
  expectedBoundHostnames: z5.array(z5.string()),
2516
- acknowledge: z5.literal("delete_site_and_cancel_renewal")
2534
+ acknowledge: z5.literal("delete_and_cancel_renewal")
2517
2535
  });
2518
2536
  var unbindConfirmation = z5.object({
2519
2537
  siteId: z5.string().min(1),
@@ -2522,11 +2540,11 @@ var unbindConfirmation = z5.object({
2522
2540
  expectedBindingStatus: z5.enum(["provisioning", "active"]),
2523
2541
  apexDomain: z5.string().min(1),
2524
2542
  expectedBoundHostnames: z5.array(z5.string()),
2525
- acknowledge: z5.literal("unbind_domain_and_remove_custom_hostnames")
2543
+ acknowledge: z5.literal("unbind_and_remove_custom_hostnames")
2526
2544
  });
2527
2545
  function registerLifecycleTools(server, baseCtx) {
2528
2546
  server.registerTool(
2529
- "delete_site",
2547
+ "delete",
2530
2548
  {
2531
2549
  description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
2532
2550
  inputSchema: {
@@ -2543,7 +2561,7 @@ function registerLifecycleTools(server, baseCtx) {
2543
2561
  const ctx = withProjectDir(baseCtx, args.projectDir);
2544
2562
  const site = requireSiteFile(ctx);
2545
2563
  if (!args.operationId) {
2546
- throw new Error("operationId is required for delete_site");
2564
+ throw new Error("operationId is required for delete");
2547
2565
  }
2548
2566
  if (args.action === "preview") {
2549
2567
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
@@ -2552,12 +2570,12 @@ function registerLifecycleTools(server, baseCtx) {
2552
2570
  return structuredToolResult({
2553
2571
  schemaVersion: 1,
2554
2572
  outcome: "waiting_user",
2555
- resultCode: "delete_site_confirmation_required",
2573
+ resultCode: "delete_confirmation_required",
2556
2574
  operationId: args.operationId,
2557
2575
  summary: "Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable.",
2558
2576
  data: { preview },
2559
2577
  nextActions: [
2560
- { tool: "delete_site", allowed: true, reasonCode: "exact_confirmation_required" }
2578
+ { tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
2561
2579
  ]
2562
2580
  });
2563
2581
  }
@@ -2583,7 +2601,7 @@ function registerLifecycleTools(server, baseCtx) {
2583
2601
  }
2584
2602
  );
2585
2603
  server.registerTool(
2586
- "unbind_domain",
2604
+ "unbind",
2587
2605
  {
2588
2606
  description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
2589
2607
  inputSchema: {
@@ -2599,7 +2617,7 @@ function registerLifecycleTools(server, baseCtx) {
2599
2617
  try {
2600
2618
  const ctx = withProjectDir(baseCtx, args.projectDir);
2601
2619
  const site = requireSiteFile(ctx);
2602
- if (!args.operationId) throw new Error("operationId is required for unbind_domain");
2620
+ if (!args.operationId) throw new Error("operationId is required for unbind");
2603
2621
  if (args.action === "preview") {
2604
2622
  const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
2605
2623
  operationId: args.operationId
@@ -2607,12 +2625,12 @@ function registerLifecycleTools(server, baseCtx) {
2607
2625
  return structuredToolResult({
2608
2626
  schemaVersion: 1,
2609
2627
  outcome: "waiting_user",
2610
- resultCode: "unbind_domain_confirmation_required",
2628
+ resultCode: "unbind_confirmation_required",
2611
2629
  operationId: args.operationId,
2612
2630
  summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
2613
2631
  data: { preview },
2614
2632
  nextActions: [
2615
- { tool: "unbind_domain", allowed: true, reasonCode: "exact_confirmation_required" }
2633
+ { tool: "unbind", allowed: true, reasonCode: "exact_confirmation_required" }
2616
2634
  ]
2617
2635
  });
2618
2636
  }
@@ -2630,7 +2648,7 @@ function registerLifecycleTools(server, baseCtx) {
2630
2648
  operationId: args.operationId,
2631
2649
  summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
2632
2650
  data: { result },
2633
- nextActions: [{ tool: "site_status", allowed: true }]
2651
+ nextActions: [{ tool: "status", allowed: true }]
2634
2652
  });
2635
2653
  } catch (error) {
2636
2654
  return toolError(error);
@@ -2715,30 +2733,30 @@ var FetchTransport = class {
2715
2733
  var instructionsFor = (hostPattern) => `Sakupa publishes AI-made static websites. AI-made pages, live in seconds.
2716
2734
 
2717
2735
  Workflow:
2718
- 1. analyze_site \u2014 check whether this project has deployable static output. If a build is needed
2736
+ 1. analyze \u2014 check whether this project has deployable static output. If a build is needed
2719
2737
  (Vite/Vue/React/Svelte/Astro/Next static export/Nuxt generate), run the build LOCALLY first,
2720
- then re-run analyze_site.
2721
- 2. deploy_site \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
2738
+ then re-run analyze.
2739
+ 2. deploy \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
2722
2740
  site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
2723
2741
  management credential in .sakupa/site.json. Deploying again updates the site and refreshes
2724
- its validity; refresh_site extends validity without uploading; site_status shows the
2742
+ its validity; refresh extends validity without uploading; status shows the
2725
2743
  current deployment and serving state at any time.
2726
- 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (list_billing_plans
2727
- shows the catalog; subscribe_site -> Stripe-hosted checkout;
2744
+ 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (plans
2745
+ shows the catalog; subscribe -> Stripe-hosted checkout;
2728
2746
  water/personal/share/business). Paying makes the
2729
2747
  ${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
2730
2748
  shows an over-limit notice by default. An external AI may periodically query usage and
2731
2749
  recommend a plan, but Sakupa never changes a subscription automatically.
2732
- 4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
2750
+ 4. Optionally bind a custom domain to the subscribed site (bind): an included extra
2733
2751
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
2734
- first verified request wins; unverified requests expire after 72 hours. billing_status,
2735
- change_subscription_plan, manage_billing and recover_domain_site manage the paid
2736
- lifecycle; unbind_domain removes the custom domain (the permanent URL keeps serving) and
2737
- delete_site tears the whole site down after explicit confirmation.
2752
+ first verified request wins; unverified requests expire after 72 hours. billing,
2753
+ upgrade, portal and recover manage the paid
2754
+ lifecycle; unbind removes the custom domain (the permanent URL keeps serving) and
2755
+ delete tears the whole site down after explicit confirmation.
2738
2756
  Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2739
2757
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
2740
2758
  24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
2741
- 5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
2759
+ 5. support (subscribed sites) opens a support ticket; report sends a
2742
2760
  sanitized diagnostic report after the user explicitly confirms it.
2743
2761
 
2744
2762
  Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
@@ -2747,12 +2765,12 @@ the user's PROJECT ROOT, the SAME directory every time for the same project: the
2747
2765
  the user opened (for framework projects, where package.json lives \u2014 never the dist/out
2748
2766
  build folder; output is auto-detected). Without it the server falls back to its startup
2749
2767
  directory, which may be a different project than the one the user is looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
2750
- Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
2751
- refresh_site, delete_site and unbind_domain echo
2768
+ Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
2769
+ refresh, delete and unbind echo
2752
2770
  the directory they acted on \u2014 verify it matches the user's active project.
2753
2771
 
2754
2772
  When the same operation fails twice in a row, or the user is clearly stuck or
2755
- frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
2773
+ frustrated, proactively offer report: it files the problem into Sakupa's ticket and
2756
2774
  alert stream, and you should attach your own factual account via agentContext.
2757
2775
 
2758
2776
  Safety boundaries:
@@ -2771,7 +2789,7 @@ Safety boundaries:
2771
2789
  price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
2772
2790
  real charge in JPY. Never show a bare Yen sign.
2773
2791
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
2774
- a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
2792
+ a bound custom domain, a lost credential is unrecoverable by design. portal then opens
2775
2793
  Stripe's public no-code portal login, where the customer verifies the checkout email with a
2776
2794
  Stripe one-time passcode; it never restores site authority.`;
2777
2795
  function createSakupaMcpServer(opts) {
@@ -2782,21 +2800,15 @@ function createSakupaMcpServer(opts) {
2782
2800
  { name: "sakupa", version: MCP_VERSION },
2783
2801
  { instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
2784
2802
  );
2785
- registerTools(server, {
2803
+ const ctx = {
2786
2804
  client,
2787
2805
  projectDir: opts.projectDir,
2788
- apiBaseUrl: opts.apiBaseUrl
2789
- });
2790
- registerBillingTools(server, {
2791
- client,
2792
- projectDir: opts.projectDir,
2793
- apiBaseUrl: opts.apiBaseUrl
2794
- });
2795
- registerLifecycleTools(server, {
2796
- client,
2797
- projectDir: opts.projectDir,
2798
- apiBaseUrl: opts.apiBaseUrl
2799
- });
2806
+ apiBaseUrl: opts.apiBaseUrl,
2807
+ session: { projectDir: null }
2808
+ };
2809
+ registerTools(server, ctx);
2810
+ registerBillingTools(server, ctx);
2811
+ registerLifecycleTools(server, ctx);
2800
2812
  return server;
2801
2813
  }
2802
2814