@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/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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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("
|
|
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
|
-
"
|
|
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 (
|
|
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 (
|
|
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
|
-
"
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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 (
|
|
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("
|
|
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
|
-
"
|
|
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: "
|
|
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
|
-
"
|
|
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
|
|
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: "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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. "
|
|
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
|
|
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("
|
|
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("
|
|
2535
|
+
acknowledge: z4.literal("unbind_and_remove_custom_hostnames")
|
|
2518
2536
|
});
|
|
2519
2537
|
function registerLifecycleTools(server, baseCtx) {
|
|
2520
2538
|
server.registerTool(
|
|
2521
|
-
"
|
|
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
|
|
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: "
|
|
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: "
|
|
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
|
-
"
|
|
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
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
"
|
|
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: "
|
|
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
|
-
"
|
|
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: "
|
|
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.
|
|
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
|
|
2716
|
-
2.
|
|
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;
|
|
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 (
|
|
2722
|
-
shows the catalog;
|
|
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 (
|
|
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.
|
|
2730
|
-
|
|
2731
|
-
lifecycle;
|
|
2732
|
-
|
|
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.
|
|
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).
|
|
2746
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
2798
|
+
const ctx = {
|
|
2781
2799
|
client,
|
|
2782
2800
|
projectDir: opts.projectDir,
|
|
2783
|
-
apiBaseUrl: opts.apiBaseUrl
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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 {
|