@sakupa/mcp 0.7.10 → 0.7.11

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 +71 -25
  2. package/dist/index.js +71 -25
  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.10";
132
+ var SAKUPA_MCP_VERSION = "0.7.11";
133
133
 
134
134
  // ../core/dist/domain/errors.js
135
135
  var HTTP_STATUS = {
@@ -686,7 +686,7 @@ var HttpApiClient = class {
686
686
 
687
687
  // src/tools/definitions.ts
688
688
  import { randomUUID } from "node:crypto";
689
- import { promises as fs2 } from "node:fs";
689
+ import { existsSync as existsSync3, promises as fs2 } from "node:fs";
690
690
  import { join as join4, resolve as resolve3 } from "node:path";
691
691
  import { z as z3 } from "zod";
692
692
 
@@ -1196,14 +1196,18 @@ function deleteSiteFile(projectDir) {
1196
1196
  rmSync(path, { force: true });
1197
1197
  }
1198
1198
  }
1199
- function isInsideGitRepo(projectDir) {
1200
- let dir = projectDir;
1201
- for (; ; ) {
1202
- if (existsSync(join2(dir, ".git"))) return true;
1203
- const parent = dirname(dir);
1204
- if (parent === dir) return false;
1205
- dir = parent;
1199
+ function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
1200
+ let cursor = startDir;
1201
+ for (let i = 0; i < maxLevels; i += 1) {
1202
+ const parent = dirname(cursor);
1203
+ if (parent === cursor) return null;
1204
+ if (predicate(parent)) return parent;
1205
+ cursor = parent;
1206
1206
  }
1207
+ return null;
1208
+ }
1209
+ function isInsideGitRepo(projectDir) {
1210
+ return existsSync(join2(projectDir, ".git")) || findAncestor(projectDir, (dir) => existsSync(join2(dir, ".git"))) !== null;
1207
1211
  }
1208
1212
  function credentialGitReminder(projectDir) {
1209
1213
  if (!isInsideGitRepo(projectDir)) return "";
@@ -1354,7 +1358,7 @@ async function diagnoseBinding(input) {
1354
1358
  const apexResolves = apexAnswers.length > 0;
1355
1359
  const allOk = checks.every((c) => c.state === "ok");
1356
1360
  const checklist = checks.map(renderCheck).join("\n") + `
1357
- [${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet: point it at ${input.servingTarget} using your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).`);
1361
+ [${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most panels and Cloudflare accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
1358
1362
  const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
1359
1363
  return { checks, apexResolves, allOk, checklist, layers };
1360
1364
  }
@@ -1437,7 +1441,7 @@ var LocalGuidanceError = class extends SakupaError {
1437
1441
  }
1438
1442
  };
1439
1443
  var projectDirInput = z2.string().optional().describe(
1440
- "Absolute path of the project directory the user is CURRENTLY working in (the folder holding the site files and .sakupa). Always pass it explicitly; when omitted the server falls back to its startup directory, which may not be where the user is working now."
1444
+ "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."
1441
1445
  );
1442
1446
  function withProjectDir(ctx, projectDirArg) {
1443
1447
  if (projectDirArg === void 0) return ctx;
@@ -1661,6 +1665,20 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
1661
1665
  };
1662
1666
  }
1663
1667
  }
1668
+ function projectRootAbove(projectDir) {
1669
+ if (existsSync3(join4(projectDir, "package.json"))) return null;
1670
+ return findAncestor(projectDir, (dir) => existsSync3(join4(dir, "package.json")), 4);
1671
+ }
1672
+ function findNeighborBinding(projectDir, outputRel) {
1673
+ const bound = (dir) => loadSiteFile(dir).kind !== "absent";
1674
+ const above = findAncestor(projectDir, bound, 3);
1675
+ if (above) return above;
1676
+ if (outputRel && outputRel !== ".") {
1677
+ const outputAbs = resolve3(projectDir, outputRel);
1678
+ if (bound(outputAbs)) return outputAbs;
1679
+ }
1680
+ return null;
1681
+ }
1664
1682
  function freeSiteCreationBarrier() {
1665
1683
  const recent = listRecentCreations(Date.now());
1666
1684
  if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
@@ -1721,6 +1739,9 @@ Next action: ${analysis.suggestedNextAction}`,
1721
1739
  publicConfirmed: z3.boolean().optional().describe(
1722
1740
  "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
1723
1741
  ),
1742
+ subprojectConfirmed: z3.boolean().optional().describe(
1743
+ "Only when creating a NEW site in a subfolder of a package.json project: the user explicitly confirmed this subfolder is an INDEPENDENT site, not the project's build output."
1744
+ ),
1724
1745
  lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
1725
1746
  }
1726
1747
  },
@@ -1749,16 +1770,34 @@ Next action: ${analysis.suggestedNextAction}`,
1749
1770
  }
1750
1771
  const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
1751
1772
  if (!existing) {
1773
+ const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
1774
+ if (rootAbove) {
1775
+ return text(
1776
+ "not_project_root",
1777
+ `${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.`,
1778
+ { projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
1779
+ "blocked"
1780
+ );
1781
+ }
1782
+ const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
1783
+ if (neighbor) {
1784
+ return text(
1785
+ "neighbor_binding_found",
1786
+ `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.`,
1787
+ { neighborProjectDir: neighbor },
1788
+ "blocked"
1789
+ );
1790
+ }
1752
1791
  const barrier = freeSiteCreationBarrier();
1753
1792
  if (barrier) return barrier;
1754
- }
1755
- if (!existing && args.publicConfirmed !== true) {
1756
- return text(
1757
- "public_deployment_confirmation_required",
1758
- `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.`,
1759
- { publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
1760
- "waiting_user"
1761
- );
1793
+ if (args.publicConfirmed !== true) {
1794
+ return text(
1795
+ "public_deployment_confirmation_required",
1796
+ `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.`,
1797
+ { publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
1798
+ "waiting_user"
1799
+ );
1800
+ }
1762
1801
  }
1763
1802
  ensureUploadSizeWithinLimits(manifest, !existing);
1764
1803
  if (!existing) {
@@ -2051,17 +2090,19 @@ ${res2.message}
2051
2090
  "domain_verification_started",
2052
2091
  `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2053
2092
 
2054
- Add ALL THREE DNS records NOW (adding them together lets verification, certificate issuance and serving complete without further record changes):
2093
+ STEP 1 of 2 \u2014 add ALL THREE records NOW:
2055
2094
 
2056
2095
  1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
2057
2096
  2) CNAME host: www value: ${res.servingTarget}
2058
- 3) APEX host: @ -> ${res.servingTarget} via your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).
2097
+ 3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
2059
2098
 
2060
2099
  Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
2061
2100
 
2062
2101
  Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2063
2102
 
2064
- Then run bind_domain with action "status" \u2014 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.`,
2103
+ 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.
2104
+
2105
+ 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.`,
2065
2106
  {
2066
2107
  verificationId: res.verificationId,
2067
2108
  apexDomain: apex,
@@ -2709,13 +2750,18 @@ Workflow:
2709
2750
 
2710
2751
  Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
2711
2752
  binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
2712
- the directory the user is currently working in, on every call. Without it the server falls
2713
- back to its startup directory, which may be a different project than the one the user is
2714
- looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
2753
+ the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
2754
+ the user opened (for framework projects, where package.json lives \u2014 never the dist/out
2755
+ build folder; output is auto-detected). Without it the server falls back to its startup
2756
+ 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
2715
2757
  Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
2716
2758
  refresh_site, delete_site and unbind_domain echo
2717
2759
  the directory they acted on \u2014 verify it matches the user's active project.
2718
2760
 
2761
+ When the same operation fails twice in a row, or the user is clearly stuck or
2762
+ frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
2763
+ alert stream, and you should attach your own factual account via agentContext.
2764
+
2719
2765
  Safety boundaries:
2720
2766
  - Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
2721
2767
  - Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
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.10";
127
+ var SAKUPA_MCP_VERSION = "0.7.11";
128
128
 
129
129
  // ../core/dist/domain/errors.js
130
130
  var HTTP_STATUS = {
@@ -839,14 +839,18 @@ function deleteSiteFile(projectDir) {
839
839
  rmSync(path, { force: true });
840
840
  }
841
841
  }
842
- function isInsideGitRepo(projectDir) {
843
- let dir = projectDir;
844
- for (; ; ) {
845
- if (existsSync(join(dir, ".git"))) return true;
846
- const parent = dirname(dir);
847
- if (parent === dir) return false;
848
- dir = parent;
842
+ function findAncestor(startDir, predicate, maxLevels = Number.POSITIVE_INFINITY) {
843
+ let cursor = startDir;
844
+ for (let i = 0; i < maxLevels; i += 1) {
845
+ const parent = dirname(cursor);
846
+ if (parent === cursor) return null;
847
+ if (predicate(parent)) return parent;
848
+ cursor = parent;
849
849
  }
850
+ return null;
851
+ }
852
+ function isInsideGitRepo(projectDir) {
853
+ return existsSync(join(projectDir, ".git")) || findAncestor(projectDir, (dir) => existsSync(join(dir, ".git"))) !== null;
850
854
  }
851
855
  function credentialGitReminder(projectDir) {
852
856
  if (!isInsideGitRepo(projectDir)) return "";
@@ -1326,7 +1330,7 @@ var LocalGuidanceError = class extends SakupaError {
1326
1330
  }
1327
1331
  };
1328
1332
  var projectDirInput = z2.string().optional().describe(
1329
- "Absolute path of the project directory the user is CURRENTLY working in (the folder holding the site files and .sakupa). Always pass it explicitly; when omitted the server falls back to its startup directory, which may not be where the user is working now."
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."
1330
1334
  );
1331
1335
  function withProjectDir(ctx, projectDirArg) {
1332
1336
  if (projectDirArg === void 0) return ctx;
@@ -1414,7 +1418,7 @@ function toolError(e) {
1414
1418
 
1415
1419
  // src/tools/definitions.ts
1416
1420
  import { randomUUID } from "node:crypto";
1417
- import { promises as fs2 } from "node:fs";
1421
+ import { existsSync as existsSync3, promises as fs2 } from "node:fs";
1418
1422
  import { join as join4, resolve as resolve3 } from "node:path";
1419
1423
  import { z as z3 } from "zod";
1420
1424
 
@@ -1562,7 +1566,7 @@ async function diagnoseBinding(input) {
1562
1566
  const apexResolves = apexAnswers.length > 0;
1563
1567
  const allOk = checks.every((c) => c.state === "ok");
1564
1568
  const checklist = checks.map(renderCheck).join("\n") + `
1565
- [${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet: point it at ${input.servingTarget} using your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).`);
1569
+ [${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet. Three-step fix, stop at the first that works: (1) try adding a plain record \u2014 type CNAME, host @, value ${input.servingTarget} (most panels and Cloudflare accept this directly; confirm past any MX-conflict warning if the domain sends no email). (2) If rejected, look for ALIAS / ANAME / CNAME-flattening in the record-type list \u2014 same host and value. (3) If the panel has neither, skip the apex: www alone works fine (certificates do not depend on the apex record); optionally add a URL redirect from @ to www.`);
1566
1570
  const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
1567
1571
  return { checks, apexResolves, allOk, checklist, layers };
1568
1572
  }
@@ -1725,6 +1729,20 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
1725
1729
  };
1726
1730
  }
1727
1731
  }
1732
+ function projectRootAbove(projectDir) {
1733
+ if (existsSync3(join4(projectDir, "package.json"))) return null;
1734
+ return findAncestor(projectDir, (dir) => existsSync3(join4(dir, "package.json")), 4);
1735
+ }
1736
+ function findNeighborBinding(projectDir, outputRel) {
1737
+ const bound = (dir) => loadSiteFile(dir).kind !== "absent";
1738
+ const above = findAncestor(projectDir, bound, 3);
1739
+ if (above) return above;
1740
+ if (outputRel && outputRel !== ".") {
1741
+ const outputAbs = resolve3(projectDir, outputRel);
1742
+ if (bound(outputAbs)) return outputAbs;
1743
+ }
1744
+ return null;
1745
+ }
1728
1746
  function freeSiteCreationBarrier() {
1729
1747
  const recent = listRecentCreations(Date.now());
1730
1748
  if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
@@ -1785,6 +1803,9 @@ Next action: ${analysis.suggestedNextAction}`,
1785
1803
  publicConfirmed: z3.boolean().optional().describe(
1786
1804
  "Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
1787
1805
  ),
1806
+ subprojectConfirmed: z3.boolean().optional().describe(
1807
+ "Only when creating a NEW site in a subfolder of a package.json project: the user explicitly confirmed this subfolder is an INDEPENDENT site, not the project's build output."
1808
+ ),
1788
1809
  lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
1789
1810
  }
1790
1811
  },
@@ -1813,16 +1834,34 @@ Next action: ${analysis.suggestedNextAction}`,
1813
1834
  }
1814
1835
  const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
1815
1836
  if (!existing) {
1837
+ const rootAbove = args.subprojectConfirmed === true ? null : projectRootAbove(ctx.projectDir);
1838
+ if (rootAbove) {
1839
+ return text(
1840
+ "not_project_root",
1841
+ `${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.`,
1842
+ { projectRoot: rootAbove, confirmationField: "subprojectConfirmed" },
1843
+ "blocked"
1844
+ );
1845
+ }
1846
+ const neighbor = findNeighborBinding(ctx.projectDir, analysis.recommendedOutputDir);
1847
+ if (neighbor) {
1848
+ return text(
1849
+ "neighbor_binding_found",
1850
+ `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.`,
1851
+ { neighborProjectDir: neighbor },
1852
+ "blocked"
1853
+ );
1854
+ }
1816
1855
  const barrier = freeSiteCreationBarrier();
1817
1856
  if (barrier) return barrier;
1818
- }
1819
- if (!existing && args.publicConfirmed !== true) {
1820
- return text(
1821
- "public_deployment_confirmation_required",
1822
- `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.`,
1823
- { publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
1824
- "waiting_user"
1825
- );
1857
+ if (args.publicConfirmed !== true) {
1858
+ return text(
1859
+ "public_deployment_confirmation_required",
1860
+ `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.`,
1861
+ { publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
1862
+ "waiting_user"
1863
+ );
1864
+ }
1826
1865
  }
1827
1866
  ensureUploadSizeWithinLimits(manifest, !existing);
1828
1867
  if (!existing) {
@@ -2115,17 +2154,19 @@ ${res2.message}
2115
2154
  "domain_verification_started",
2116
2155
  `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
2117
2156
 
2118
- Add ALL THREE DNS records NOW (adding them together lets verification, certificate issuance and serving complete without further record changes):
2157
+ STEP 1 of 2 \u2014 add ALL THREE records NOW:
2119
2158
 
2120
2159
  1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
2121
2160
  2) CNAME host: www value: ${res.servingTarget}
2122
- 3) APEX host: @ -> ${res.servingTarget} via your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).
2161
+ 3) APEX host: @ -> ${res.servingTarget}. Try a plain CNAME at host @ first (most panels accept it); if rejected use ALIAS / ANAME / CNAME-flattening; if the panel has neither, skip it \u2014 www alone works, and a URL redirect from @ to www covers bare-domain visitors.
2123
2162
 
2124
2163
  Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
2125
2164
 
2126
2165
  Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
2127
2166
 
2128
- Then run bind_domain with action "status" \u2014 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.`,
2167
+ 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.
2168
+
2169
+ 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.`,
2129
2170
  {
2130
2171
  verificationId: res.verificationId,
2131
2172
  apexDomain: apex,
@@ -2704,13 +2745,18 @@ Workflow:
2704
2745
 
2705
2746
  Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
2706
2747
  binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
2707
- the directory the user is currently working in, on every call. Without it the server falls
2708
- back to its startup directory, which may be a different project than the one the user is
2709
- looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
2748
+ the user's PROJECT ROOT, the SAME directory every time for the same project: the folder
2749
+ the user opened (for framework projects, where package.json lives \u2014 never the dist/out
2750
+ build folder; output is auto-detected). Without it the server falls back to its startup
2751
+ 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
2710
2752
  Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
2711
2753
  refresh_site, delete_site and unbind_domain echo
2712
2754
  the directory they acted on \u2014 verify it matches the user's active project.
2713
2755
 
2756
+ When the same operation fails twice in a row, or the user is clearly stuck or
2757
+ frustrated, proactively offer report_bug: it files the problem into Sakupa's ticket and
2758
+ alert stream, and you should attach your own factual account via agentContext.
2759
+
2714
2760
  Safety boundaries:
2715
2761
  - Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
2716
2762
  - Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",