@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.
- package/dist/bin.js +111 -99
- package/dist/index.js +111 -99
- 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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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("
|
|
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
|
-
"
|
|
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 (
|
|
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 (
|
|
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
|
-
"
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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 (
|
|
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("
|
|
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
|
-
"
|
|
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: "
|
|
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
|
-
"
|
|
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
|
|
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: "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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. "
|
|
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
|
|
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
|
-
"
|
|
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: "
|
|
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
|
-
"
|
|
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: "
|
|
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("
|
|
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("
|
|
2543
|
+
acknowledge: z5.literal("unbind_and_remove_custom_hostnames")
|
|
2526
2544
|
});
|
|
2527
2545
|
function registerLifecycleTools(server, baseCtx) {
|
|
2528
2546
|
server.registerTool(
|
|
2529
|
-
"
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
-
"
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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.
|
|
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
|
|
2721
|
-
2.
|
|
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;
|
|
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 (
|
|
2727
|
-
shows the catalog;
|
|
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 (
|
|
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.
|
|
2735
|
-
|
|
2736
|
-
lifecycle;
|
|
2737
|
-
|
|
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.
|
|
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).
|
|
2751
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
2803
|
+
const ctx = {
|
|
2786
2804
|
client,
|
|
2787
2805
|
projectDir: opts.projectDir,
|
|
2788
|
-
apiBaseUrl: opts.apiBaseUrl
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
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
|
|