@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/index.js CHANGED
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
124
124
  var ALLOWED_HIDDEN_PATHS = [".well-known/"];
125
125
 
126
126
  // ../core/dist/domain/version.js
127
- var SAKUPA_MCP_VERSION = "0.7.13";
127
+ var SAKUPA_MCP_VERSION = "0.7.14";
128
128
 
129
129
  // ../core/dist/domain/errors.js
130
130
  var HTTP_STATUS = {
@@ -1196,13 +1196,13 @@ async function analyzeProject(projectDir, opts = {}) {
1196
1196
  const sourceWithoutBuild = pkg !== null && buildRequired && (await isDirectory(join2(root, "src")) || await isDirectory(join2(root, "pages")));
1197
1197
  let suggestedNextAction2;
1198
1198
  if (opts.outputDir !== void 0) {
1199
- 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.`;
1199
+ 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.`;
1200
1200
  } else if (ssrRisks.length > 0 && detection) {
1201
- 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.`;
1201
+ 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.`;
1202
1202
  } else if (sourceWithoutBuild || detection && !outputDirExists) {
1203
- suggestedNextAction2 = `This looks like a source project, not built static output. Run the build locally (${buildCommandHint ?? "npm run build"}) then re-run analyze_site.`;
1203
+ suggestedNextAction2 = `This looks like a source project, not built static output. Run the build locally (${buildCommandHint ?? "npm run build"}) then re-run analyze.`;
1204
1204
  } else {
1205
- 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.";
1205
+ 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.";
1206
1206
  }
1207
1207
  return {
1208
1208
  projectType,
@@ -1247,14 +1247,14 @@ async function analyzeProject(projectDir, opts = {}) {
1247
1247
  if (!deployable) {
1248
1248
  const firstError = validation.issues.find((i) => i.severity === "error");
1249
1249
  if (firstError?.code === "missing_index_html") {
1250
- 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.`;
1250
+ 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.`;
1251
1251
  } else {
1252
- suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze_site.";
1252
+ suggestedNextAction = "Fix the listed issues (remove forbidden/secret files, reduce size, add missing entry HTML), then re-run analyze.";
1253
1253
  }
1254
1254
  } else if (spa.looksLikeSpa) {
1255
- 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.`;
1255
+ 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.`;
1256
1256
  } else {
1257
- suggestedNextAction = `Run deploy_site to publish the static output in "${outputDirRel}".`;
1257
+ suggestedNextAction = `Run deploy to publish the static output in "${outputDirRel}".`;
1258
1258
  }
1259
1259
  return {
1260
1260
  projectType,
@@ -1333,7 +1333,10 @@ var projectDirInput = z2.string().optional().describe(
1333
1333
  "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."
1334
1334
  );
1335
1335
  function withProjectDir(ctx, projectDirArg) {
1336
- if (projectDirArg === void 0) return ctx;
1336
+ if (projectDirArg === void 0) {
1337
+ const sticky = ctx.session.projectDir;
1338
+ return sticky !== null ? { ...ctx, projectDir: sticky } : ctx;
1339
+ }
1337
1340
  if (!isAbsolute(projectDirArg)) {
1338
1341
  throw new LocalGuidanceError(
1339
1342
  "invalid_request",
@@ -1354,6 +1357,7 @@ function withProjectDir(ctx, projectDirArg) {
1354
1357
  `projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
1355
1358
  );
1356
1359
  }
1360
+ ctx.session.projectDir = dir;
1357
1361
  return { ...ctx, projectDir: dir };
1358
1362
  }
1359
1363
  function requireSiteFile(ctx) {
@@ -1367,7 +1371,7 @@ function requireSiteFile(ctx) {
1367
1371
  if (state.kind === "absent") {
1368
1372
  throw new LocalGuidanceError(
1369
1373
  "not_found",
1370
- `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.`
1374
+ `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.`
1371
1375
  );
1372
1376
  }
1373
1377
  return state.file;
@@ -1442,13 +1446,15 @@ function writeAll(records) {
1442
1446
  writeFileSync2(path, `${JSON.stringify(records, null, 2)}
1443
1447
  `, "utf-8");
1444
1448
  }
1445
- function listRecentCreations(nowMs) {
1449
+ function listRecentCreations(nowMs, apiBaseUrl) {
1446
1450
  return readAll().filter((e) => {
1447
1451
  const t = Date.parse(e.createdAt);
1448
- return Number.isFinite(t) && nowMs - t < RECENT_WINDOW_MS;
1452
+ if (!Number.isFinite(t) || nowMs - t >= RECENT_WINDOW_MS) return false;
1453
+ return e.apiBaseUrl === void 0 || e.apiBaseUrl === apiBaseUrl;
1449
1454
  });
1450
1455
  }
1451
1456
  function recordCreation(record) {
1457
+ knownQuotaFree.delete(record.siteId);
1452
1458
  const rest = readAll().filter((e) => e.siteId !== record.siteId);
1453
1459
  writeAll([...rest, record]);
1454
1460
  }
@@ -1457,6 +1463,12 @@ function removeCreation(siteId) {
1457
1463
  const rest = all.filter((e) => e.siteId !== siteId);
1458
1464
  if (rest.length !== all.length) writeAll(rest);
1459
1465
  }
1466
+ var knownQuotaFree = /* @__PURE__ */ new Set();
1467
+ function noteSiteMode(siteId, mode) {
1468
+ if (mode !== "paid" || knownQuotaFree.has(siteId)) return;
1469
+ removeCreation(siteId);
1470
+ knownQuotaFree.add(siteId);
1471
+ }
1460
1472
 
1461
1473
  // src/dns-doh.ts
1462
1474
  var dohFetch = (input, init) => fetch(input, init);
@@ -1682,7 +1694,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
1682
1694
  if (bytes.byteLength !== match.size) {
1683
1695
  throw new SakupaError(
1684
1696
  "validation_failed",
1685
- `"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy_site so Sakupa re-checks the current files before uploading.`
1697
+ `"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy so Sakupa re-checks the current files before uploading.`
1686
1698
  );
1687
1699
  }
1688
1700
  await ctx.client.uploadFile(target, bytes);
@@ -1697,7 +1709,7 @@ Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provision
1697
1709
  check = await client.checkVerification(pb.verificationId, credential);
1698
1710
  } catch {
1699
1711
  return {
1700
- note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try site_status again shortly.)"
1712
+ note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try status again shortly.)"
1701
1713
  };
1702
1714
  }
1703
1715
  try {
@@ -1717,7 +1729,7 @@ Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provision
1717
1729
  ${block}`, checklist: toDnsChecklist(diag.checks) };
1718
1730
  } catch {
1719
1731
  return {
1720
- note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind_domain with action "status" for the per-record checklist.)'
1732
+ note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind with action "status" for the per-record checklist.)'
1721
1733
  };
1722
1734
  }
1723
1735
  }
@@ -1735,17 +1747,19 @@ function findNeighborBinding(projectDir, outputRel) {
1735
1747
  }
1736
1748
  return null;
1737
1749
  }
1738
- function freeSiteCreationBarrier() {
1739
- const recent = listRecentCreations(Date.now());
1750
+ function freeSiteCreationBarrier(apiBaseUrl) {
1751
+ const recent = listRecentCreations(Date.now(), apiBaseUrl);
1740
1752
  if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
1741
1753
  const registryPath = creationRegistryPath();
1742
1754
  return text(
1743
1755
  "local_site_limit_reached",
1744
- `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.
1756
+ `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.
1745
1757
 
1746
1758
  ` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
1747
1759
 
1748
- 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.`,
1760
+ 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.
1761
+
1762
+ 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.`,
1749
1763
  { recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
1750
1764
  "blocked"
1751
1765
  );
@@ -1753,9 +1767,9 @@ Options: delete one of these sites (run delete_site with its projectDir), wait f
1753
1767
  function registerTools(server, baseCtx) {
1754
1768
  const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
1755
1769
  server.registerTool(
1756
- "analyze_site",
1770
+ "analyze",
1757
1771
  {
1758
- 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.",
1772
+ 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.",
1759
1773
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1760
1774
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
1761
1775
  inputSchema: {
@@ -1781,9 +1795,9 @@ Next action: ${analysis.suggestedNextAction}`,
1781
1795
  }
1782
1796
  );
1783
1797
  server.registerTool(
1784
- "deploy_site",
1798
+ "deploy",
1785
1799
  {
1786
- 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.`,
1800
+ 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.`,
1787
1801
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
1788
1802
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1789
1803
  inputSchema: {
@@ -1830,7 +1844,7 @@ Next action: ${analysis.suggestedNextAction}`,
1830
1844
  if (rootAbove) {
1831
1845
  return text(
1832
1846
  "not_project_root",
1833
- `${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.`,
1847
+ `${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.`,
1834
1848
  { projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
1835
1849
  "blocked"
1836
1850
  );
@@ -1839,17 +1853,17 @@ Next action: ${analysis.suggestedNextAction}`,
1839
1853
  if (neighbor) {
1840
1854
  return text(
1841
1855
  "neighbor_binding_found",
1842
- `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.`,
1856
+ `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.`,
1843
1857
  { neighborProjectDir: neighbor },
1844
1858
  "blocked"
1845
1859
  );
1846
1860
  }
1847
- const barrier = freeSiteCreationBarrier();
1861
+ const barrier = freeSiteCreationBarrier(ctx.apiBaseUrl);
1848
1862
  if (barrier) return barrier;
1849
1863
  if (args.publicConfirmed !== true) {
1850
1864
  return text(
1851
1865
  "public_deployment_confirmation_required",
1852
- `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.`,
1866
+ `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.`,
1853
1867
  { publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
1854
1868
  "waiting_user"
1855
1869
  );
@@ -1881,7 +1895,8 @@ Next action: ${analysis.suggestedNextAction}`,
1881
1895
  siteId: created.siteId,
1882
1896
  projectDir: ctx.projectDir,
1883
1897
  url: finalized2.url,
1884
- createdAt
1898
+ createdAt,
1899
+ apiBaseUrl: ctx.apiBaseUrl
1885
1900
  });
1886
1901
  return text(
1887
1902
  "site_published",
@@ -1891,7 +1906,7 @@ Project directory: ${ctx.projectDir}
1891
1906
  Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
1892
1907
  ` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
1893
1908
  ` : "") + `
1894
- 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.
1909
+ 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.
1895
1910
  ` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
1896
1911
  Warnings:
1897
1912
  ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
@@ -1950,6 +1965,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
1950
1965
  }
1951
1966
  const { uploaded, finalized } = update;
1952
1967
  writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
1968
+ noteSiteMode(existing.siteId, finalized.mode);
1953
1969
  return text(
1954
1970
  "site_updated",
1955
1971
  `Site updated: ${finalized.url}
@@ -1958,7 +1974,7 @@ Project directory: ${ctx.projectDir}
1958
1974
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
1959
1975
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
1960
1976
  ` : "") + (finalized.mode === "free" ? `
1961
- 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.
1977
+ Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
1962
1978
  ` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
1963
1979
  Warnings:
1964
1980
  ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
@@ -1980,7 +1996,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1980
1996
  }
1981
1997
  );
1982
1998
  server.registerTool(
1983
- "refresh_site",
1999
+ "refresh",
1984
2000
  {
1985
2001
  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.",
1986
2002
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -1995,7 +2011,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
1995
2011
  return text(
1996
2012
  "site_refreshed",
1997
2013
  `Site validity refreshed (project: ${ctx.projectDir}). New expiry: ${res.expiresAt}
1998
- 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.`,
2014
+ 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.`,
1999
2015
  { siteId: site.siteId, expiresAt: res.expiresAt, projectDir: ctx.projectDir }
2000
2016
  );
2001
2017
  } catch (e) {
@@ -2004,7 +2020,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
2004
2020
  }
2005
2021
  );
2006
2022
  server.registerTool(
2007
- "site_status",
2023
+ "status",
2008
2024
  {
2009
2025
  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.",
2010
2026
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2016,8 +2032,9 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
2016
2032
  const ctx = withProjectDir(baseCtx, args.projectDir);
2017
2033
  const site = requireSiteFile(ctx);
2018
2034
  const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
2035
+ noteSiteMode(res.siteId, res.mode);
2019
2036
  const binding = res.pendingDomainBinding ? await describePendingBinding(ctx.client, site.credential, res.pendingDomainBinding) : void 0;
2020
- return textJson("site_status_returned", `Site status:${binding?.note ?? ""}`, {
2037
+ return textJson("status_returned", `Site status:${binding?.note ?? ""}`, {
2021
2038
  ...res,
2022
2039
  projectDir: ctx.projectDir,
2023
2040
  ...binding?.checklist ? { dnsChecklist: binding.checklist } : {}
@@ -2028,9 +2045,9 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
2028
2045
  }
2029
2046
  );
2030
2047
  server.registerTool(
2031
- "subscribe_site",
2048
+ "subscribe",
2032
2049
  {
2033
- 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.`,
2050
+ 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.`,
2034
2051
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2035
2052
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
2036
2053
  inputSchema: {
@@ -2058,7 +2075,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
2058
2075
  ${res.checkoutUrl}
2059
2076
 
2060
2077
  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.
2061
- Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind_domain) is optional and still requires DNS verification.`,
2078
+ Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind) is optional and still requires DNS verification.`,
2062
2079
  {
2063
2080
  siteId: res.siteId,
2064
2081
  plan: res.plan,
@@ -2075,9 +2092,9 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
2075
2092
  }
2076
2093
  );
2077
2094
  server.registerTool(
2078
- "bind_domain",
2095
+ "bind",
2079
2096
  {
2080
- 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.`,
2097
+ 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.`,
2081
2098
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2082
2099
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
2083
2100
  inputSchema: {
@@ -2115,7 +2132,7 @@ ${res2.message}
2115
2132
 
2116
2133
  ` + renderChecklistBlock(
2117
2134
  diag,
2118
- "Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status; if still failing after the attempts below, show the user this checklist."
2135
+ "Fix any [MISSING]/[FIX] lines above, then re-run bind status; if still failing after the attempts below, show the user this checklist."
2119
2136
  ),
2120
2137
  {
2121
2138
  verificationId: res2.verificationId,
@@ -2156,9 +2173,9 @@ Host fields above are the SHORT form: most DNS panels append the domain automati
2156
2173
 
2157
2174
  Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2158
2175
 
2159
- 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.
2176
+ 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.
2160
2177
 
2161
- 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.`,
2178
+ 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.`,
2162
2179
  {
2163
2180
  verificationId: res.verificationId,
2164
2181
  apexDomain: apex,
@@ -2187,7 +2204,7 @@ Drive the whole flow with bind_domain "status": it live-checks every record and
2187
2204
  }
2188
2205
  );
2189
2206
  server.registerTool(
2190
- "billing_status",
2207
+ "billing",
2191
2208
  {
2192
2209
  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).",
2193
2210
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2199,6 +2216,7 @@ Drive the whole flow with bind_domain "status": it live-checks every record and
2199
2216
  const ctx = withProjectDir(baseCtx, args.projectDir);
2200
2217
  const site = requireSiteFile(ctx);
2201
2218
  const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
2219
+ noteSiteMode(res.siteId, res.mode);
2202
2220
  const lines = [
2203
2221
  `Billing status for site ${res.siteId} (mode: ${res.mode})`,
2204
2222
  res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
@@ -2209,9 +2227,9 @@ Drive the whole flow with bind_domain "status": it live-checks every record and
2209
2227
  res.estimatedUsageTier ? `Estimated usage tier: ${res.estimatedUsageTier}` : void 0,
2210
2228
  res.lastReconciledAt ? `Last usage reconciliation: ${res.lastReconciledAt}` : void 0,
2211
2229
  res.boundHostnames.length > 0 ? `Bound custom domains: ${res.boundHostnames.join(", ")}` : void 0,
2212
- 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
2230
+ 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
2213
2231
  ].filter((l) => l !== void 0);
2214
- return textJson("billing_status_returned", `${lines.join("\n")}
2232
+ return textJson("billing_returned", `${lines.join("\n")}
2215
2233
 
2216
2234
  Full status:`, res);
2217
2235
  } catch (e) {
@@ -2220,7 +2238,7 @@ Full status:`, res);
2220
2238
  }
2221
2239
  );
2222
2240
  server.registerTool(
2223
- "manage_billing",
2241
+ "portal",
2224
2242
  {
2225
2243
  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.",
2226
2244
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2248,7 +2266,7 @@ Full status:`, res);
2248
2266
  url: res2.portalUrl,
2249
2267
  expectedOutcome: "The user manages payment methods, invoices, or cancels renewal on the Stripe-hosted page"
2250
2268
  },
2251
- nextActions: [{ tool: "billing_status", allowed: true }]
2269
+ nextActions: [{ tool: "billing", allowed: true }]
2252
2270
  });
2253
2271
  }
2254
2272
  const res = await ctx.client.getPublicBillingPortal();
@@ -2277,7 +2295,7 @@ Full status:`, res);
2277
2295
  }
2278
2296
  );
2279
2297
  server.registerTool(
2280
- "recover_domain_site",
2298
+ "recover",
2281
2299
  {
2282
2300
  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).",
2283
2301
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2311,7 +2329,7 @@ ${res2.message}
2311
2329
 
2312
2330
  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.
2313
2331
 
2314
- After the DNS record resolves, re-run recover_domain_site with verificationId: "${res2.verificationId}".`,
2332
+ After the DNS record resolves, re-run recover with verificationId: "${res2.verificationId}".`,
2315
2333
  {
2316
2334
  verificationId: res2.verificationId,
2317
2335
  apexDomain: res2.apexDomain,
@@ -2337,7 +2355,7 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
2337
2355
  data: { recovery: res2 },
2338
2356
  nextActions: [
2339
2357
  {
2340
- tool: "recover_domain_site",
2358
+ tool: "recover",
2341
2359
  arguments: { action: "complete", verificationId: args.verificationId },
2342
2360
  allowed: res2.readyToComplete,
2343
2361
  ...res2.readyToComplete ? {} : { reasonCode: res2.status }
@@ -2384,7 +2402,7 @@ ${res.archiveUrl}`,
2384
2402
  }
2385
2403
  );
2386
2404
  server.registerTool(
2387
- "create_support_ticket",
2405
+ "support",
2388
2406
  {
2389
2407
  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.",
2390
2408
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
@@ -2419,14 +2437,14 @@ ${res.archiveUrl}`,
2419
2437
  }
2420
2438
  );
2421
2439
  server.registerTool(
2422
- "report_bug",
2440
+ "report",
2423
2441
  {
2424
2442
  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.",
2425
2443
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
2426
2444
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
2427
2445
  inputSchema: {
2428
2446
  projectDir: projectDirInput,
2429
- toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
2447
+ toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
2430
2448
  errorCode: z3.string().optional(),
2431
2449
  errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
2432
2450
  requestId: z3.string().optional(),
@@ -2471,7 +2489,7 @@ ${res.archiveUrl}`,
2471
2489
  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). ";
2472
2490
  return textJson(
2473
2491
  "bug_report_preview_ready",
2474
- `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.`,
2492
+ `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.`,
2475
2493
  payload,
2476
2494
  "preview"
2477
2495
  );
@@ -2505,7 +2523,7 @@ var deleteConfirmation = z4.object({
2505
2523
  expectedCurrentPeriodEnd: z4.string().datetime().optional(),
2506
2524
  expectedLastDeploymentId: z4.string().optional(),
2507
2525
  expectedBoundHostnames: z4.array(z4.string()),
2508
- acknowledge: z4.literal("delete_site_and_cancel_renewal")
2526
+ acknowledge: z4.literal("delete_and_cancel_renewal")
2509
2527
  });
2510
2528
  var unbindConfirmation = z4.object({
2511
2529
  siteId: z4.string().min(1),
@@ -2514,11 +2532,11 @@ var unbindConfirmation = z4.object({
2514
2532
  expectedBindingStatus: z4.enum(["provisioning", "active"]),
2515
2533
  apexDomain: z4.string().min(1),
2516
2534
  expectedBoundHostnames: z4.array(z4.string()),
2517
- acknowledge: z4.literal("unbind_domain_and_remove_custom_hostnames")
2535
+ acknowledge: z4.literal("unbind_and_remove_custom_hostnames")
2518
2536
  });
2519
2537
  function registerLifecycleTools(server, baseCtx) {
2520
2538
  server.registerTool(
2521
- "delete_site",
2539
+ "delete",
2522
2540
  {
2523
2541
  description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
2524
2542
  inputSchema: {
@@ -2535,7 +2553,7 @@ function registerLifecycleTools(server, baseCtx) {
2535
2553
  const ctx = withProjectDir(baseCtx, args.projectDir);
2536
2554
  const site = requireSiteFile(ctx);
2537
2555
  if (!args.operationId) {
2538
- throw new Error("operationId is required for delete_site");
2556
+ throw new Error("operationId is required for delete");
2539
2557
  }
2540
2558
  if (args.action === "preview") {
2541
2559
  const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
@@ -2544,12 +2562,12 @@ function registerLifecycleTools(server, baseCtx) {
2544
2562
  return structuredToolResult({
2545
2563
  schemaVersion: 1,
2546
2564
  outcome: "waiting_user",
2547
- resultCode: "delete_site_confirmation_required",
2565
+ resultCode: "delete_confirmation_required",
2548
2566
  operationId: args.operationId,
2549
2567
  summary: "Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable.",
2550
2568
  data: { preview },
2551
2569
  nextActions: [
2552
- { tool: "delete_site", allowed: true, reasonCode: "exact_confirmation_required" }
2570
+ { tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
2553
2571
  ]
2554
2572
  });
2555
2573
  }
@@ -2575,7 +2593,7 @@ function registerLifecycleTools(server, baseCtx) {
2575
2593
  }
2576
2594
  );
2577
2595
  server.registerTool(
2578
- "unbind_domain",
2596
+ "unbind",
2579
2597
  {
2580
2598
  description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
2581
2599
  inputSchema: {
@@ -2591,7 +2609,7 @@ function registerLifecycleTools(server, baseCtx) {
2591
2609
  try {
2592
2610
  const ctx = withProjectDir(baseCtx, args.projectDir);
2593
2611
  const site = requireSiteFile(ctx);
2594
- if (!args.operationId) throw new Error("operationId is required for unbind_domain");
2612
+ if (!args.operationId) throw new Error("operationId is required for unbind");
2595
2613
  if (args.action === "preview") {
2596
2614
  const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
2597
2615
  operationId: args.operationId
@@ -2599,12 +2617,12 @@ function registerLifecycleTools(server, baseCtx) {
2599
2617
  return structuredToolResult({
2600
2618
  schemaVersion: 1,
2601
2619
  outcome: "waiting_user",
2602
- resultCode: "unbind_domain_confirmation_required",
2620
+ resultCode: "unbind_confirmation_required",
2603
2621
  operationId: args.operationId,
2604
2622
  summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
2605
2623
  data: { preview },
2606
2624
  nextActions: [
2607
- { tool: "unbind_domain", allowed: true, reasonCode: "exact_confirmation_required" }
2625
+ { tool: "unbind", allowed: true, reasonCode: "exact_confirmation_required" }
2608
2626
  ]
2609
2627
  });
2610
2628
  }
@@ -2622,7 +2640,7 @@ function registerLifecycleTools(server, baseCtx) {
2622
2640
  operationId: args.operationId,
2623
2641
  summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
2624
2642
  data: { result },
2625
- nextActions: [{ tool: "site_status", allowed: true }]
2643
+ nextActions: [{ tool: "status", allowed: true }]
2626
2644
  });
2627
2645
  } catch (error) {
2628
2646
  return toolError(error);
@@ -2639,7 +2657,7 @@ import { z as z5 } from "zod";
2639
2657
  var plan = z5.enum(["water", "personal", "share", "business"]);
2640
2658
  function registerBillingTools(server, baseCtx) {
2641
2659
  server.registerTool(
2642
- "list_billing_plans",
2660
+ "plans",
2643
2661
  {
2644
2662
  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.",
2645
2663
  inputSchema: { projectDir: projectDirInput },
@@ -2656,7 +2674,7 @@ function registerBillingTools(server, baseCtx) {
2656
2674
  resultCode: "billing_catalog_returned",
2657
2675
  summary: `Returned ${catalog.plans.length} monthly plans; the Stripe-hosted page is the final confirmation surface for payment and plan changes.`,
2658
2676
  data: { catalog },
2659
- nextActions: [{ tool: "subscribe_site", allowed: true }]
2677
+ nextActions: [{ tool: "subscribe", allowed: true }]
2660
2678
  });
2661
2679
  } catch (error) {
2662
2680
  return toolError(error);
@@ -2664,7 +2682,7 @@ function registerBillingTools(server, baseCtx) {
2664
2682
  }
2665
2683
  );
2666
2684
  server.registerTool(
2667
- "change_subscription_plan",
2685
+ "upgrade",
2668
2686
  {
2669
2687
  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.",
2670
2688
  inputSchema: {
@@ -2697,7 +2715,7 @@ function registerBillingTools(server, baseCtx) {
2697
2715
  url: result.portalUrl,
2698
2716
  expectedOutcome: "After the user confirms on the Stripe-hosted page, the webhook updates the Sakupa subscription state"
2699
2717
  },
2700
- nextActions: [{ tool: "billing_status", allowed: true }]
2718
+ nextActions: [{ tool: "billing", allowed: true }]
2701
2719
  });
2702
2720
  } catch (error) {
2703
2721
  return toolError(error);
@@ -2710,30 +2728,30 @@ function registerBillingTools(server, baseCtx) {
2710
2728
  var instructionsFor = (hostPattern) => `Sakupa publishes AI-made static websites. AI-made pages, live in seconds.
2711
2729
 
2712
2730
  Workflow:
2713
- 1. analyze_site \u2014 check whether this project has deployable static output. If a build is needed
2731
+ 1. analyze \u2014 check whether this project has deployable static output. If a build is needed
2714
2732
  (Vite/Vue/React/Svelte/Astro/Next static export/Nuxt generate), run the build LOCALLY first,
2715
- then re-run analyze_site.
2716
- 2. deploy_site \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
2733
+ then re-run analyze.
2734
+ 2. deploy \u2014 uploads ONLY the built static output. The first deploy creates a free temporary
2717
2735
  site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
2718
2736
  management credential in .sakupa/site.json. Deploying again updates the site and refreshes
2719
- its validity; refresh_site extends validity without uploading; site_status shows the
2737
+ its validity; refresh extends validity without uploading; status shows the
2720
2738
  current deployment and serving state at any time.
2721
- 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (list_billing_plans
2722
- shows the catalog; subscribe_site -> Stripe-hosted checkout;
2739
+ 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (plans
2740
+ shows the catalog; subscribe -> Stripe-hosted checkout;
2723
2741
  water/personal/share/business). Paying makes the
2724
2742
  ${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
2725
2743
  shows an over-limit notice by default. An external AI may periodically query usage and
2726
2744
  recommend a plan, but Sakupa never changes a subscription automatically.
2727
- 4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
2745
+ 4. Optionally bind a custom domain to the subscribed site (bind): an included extra
2728
2746
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
2729
- first verified request wins; unverified requests expire after 72 hours. billing_status,
2730
- change_subscription_plan, manage_billing and recover_domain_site manage the paid
2731
- lifecycle; unbind_domain removes the custom domain (the permanent URL keeps serving) and
2732
- delete_site tears the whole site down after explicit confirmation.
2747
+ first verified request wins; unverified requests expire after 72 hours. billing,
2748
+ upgrade, portal and recover manage the paid
2749
+ lifecycle; unbind removes the custom domain (the permanent URL keeps serving) and
2750
+ delete tears the whole site down after explicit confirmation.
2733
2751
  Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
2734
2752
  A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
2735
2753
  24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
2736
- 5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
2754
+ 5. support (subscribed sites) opens a support ticket; report sends a
2737
2755
  sanitized diagnostic report after the user explicitly confirms it.
2738
2756
 
2739
2757
  Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
@@ -2742,12 +2760,12 @@ the user's PROJECT ROOT, the SAME directory every time for the same project: the
2742
2760
  the user opened (for framework projects, where package.json lives \u2014 never the dist/out
2743
2761
  build folder; output is auto-detected). Without it the server falls back to its startup
2744
2762
  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
2745
- Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
2746
- refresh_site, delete_site and unbind_domain echo
2763
+ Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
2764
+ refresh, delete and unbind echo
2747
2765
  the directory they acted on \u2014 verify it matches the user's active project.
2748
2766
 
2749
2767
  When the same operation fails twice in a row, or the user is clearly stuck or
2750
- frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
2768
+ frustrated, proactively offer report: it files the problem into Sakupa's ticket and
2751
2769
  alert stream, and you should attach your own factual account via agentContext.
2752
2770
 
2753
2771
  Safety boundaries:
@@ -2766,7 +2784,7 @@ Safety boundaries:
2766
2784
  price next to the JPY amount, clearly marked as an estimate \u2014 Stripe always settles the
2767
2785
  real charge in JPY. Never show a bare Yen sign.
2768
2786
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
2769
- a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
2787
+ a bound custom domain, a lost credential is unrecoverable by design. portal then opens
2770
2788
  Stripe's public no-code portal login, where the customer verifies the checkout email with a
2771
2789
  Stripe one-time passcode; it never restores site authority.`;
2772
2790
  function createSakupaMcpServer(opts) {
@@ -2777,21 +2795,15 @@ function createSakupaMcpServer(opts) {
2777
2795
  { name: "sakupa", version: MCP_VERSION },
2778
2796
  { instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
2779
2797
  );
2780
- registerTools(server, {
2798
+ const ctx = {
2781
2799
  client,
2782
2800
  projectDir: opts.projectDir,
2783
- apiBaseUrl: opts.apiBaseUrl
2784
- });
2785
- registerBillingTools(server, {
2786
- client,
2787
- projectDir: opts.projectDir,
2788
- apiBaseUrl: opts.apiBaseUrl
2789
- });
2790
- registerLifecycleTools(server, {
2791
- client,
2792
- projectDir: opts.projectDir,
2793
- apiBaseUrl: opts.apiBaseUrl
2794
- });
2801
+ apiBaseUrl: opts.apiBaseUrl,
2802
+ session: { projectDir: null }
2803
+ };
2804
+ registerTools(server, ctx);
2805
+ registerBillingTools(server, ctx);
2806
+ registerLifecycleTools(server, ctx);
2795
2807
  return server;
2796
2808
  }
2797
2809
  export {