@sakupa/mcp 0.3.0 → 0.5.0
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 +85 -26
- package/dist/index.js +88 -29
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -126,6 +126,9 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
126
126
|
];
|
|
127
127
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
128
128
|
|
|
129
|
+
// ../core/dist/domain/version.js
|
|
130
|
+
var SAKUPA_MCP_VERSION = "0.5.0";
|
|
131
|
+
|
|
129
132
|
// ../core/dist/domain/errors.js
|
|
130
133
|
var HTTP_STATUS = {
|
|
131
134
|
invalid_request: 400,
|
|
@@ -461,6 +464,12 @@ var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
|
461
464
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
462
465
|
var utf8Encoder = new TextEncoder();
|
|
463
466
|
|
|
467
|
+
// ../core/dist/services/subscriptions.js
|
|
468
|
+
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
469
|
+
|
|
470
|
+
// ../core/dist/services/billing-cancellation.js
|
|
471
|
+
var encoder = new TextEncoder();
|
|
472
|
+
|
|
464
473
|
// src/server.ts
|
|
465
474
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
466
475
|
|
|
@@ -609,6 +618,13 @@ var HttpApiClient = class {
|
|
|
609
618
|
{ credential, body: req }
|
|
610
619
|
);
|
|
611
620
|
}
|
|
621
|
+
async requestBillingCancellation(req) {
|
|
622
|
+
return this.call(
|
|
623
|
+
"POST",
|
|
624
|
+
"/v1/billing/cancellation-requests",
|
|
625
|
+
{ body: req }
|
|
626
|
+
);
|
|
627
|
+
}
|
|
612
628
|
async createTicket(credential, req) {
|
|
613
629
|
return this.call("POST", "/v1/support/tickets", {
|
|
614
630
|
credential,
|
|
@@ -1057,7 +1073,7 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1057
1073
|
|
|
1058
1074
|
// src/project-file.ts
|
|
1059
1075
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1060
|
-
import { join as join2 } from "node:path";
|
|
1076
|
+
import { dirname, join as join2 } from "node:path";
|
|
1061
1077
|
var SITE_DIR = ".sakupa";
|
|
1062
1078
|
var SITE_FILE = "site.json";
|
|
1063
1079
|
function siteFilePath(projectDir) {
|
|
@@ -1100,9 +1116,22 @@ function writeSiteFile(projectDir, file) {
|
|
|
1100
1116
|
} catch {
|
|
1101
1117
|
}
|
|
1102
1118
|
}
|
|
1119
|
+
function isInsideGitRepo(projectDir) {
|
|
1120
|
+
let dir = projectDir;
|
|
1121
|
+
for (; ; ) {
|
|
1122
|
+
if (existsSync(join2(dir, ".git"))) return true;
|
|
1123
|
+
const parent = dirname(dir);
|
|
1124
|
+
if (parent === dir) return false;
|
|
1125
|
+
dir = parent;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function credentialGitReminder(projectDir) {
|
|
1129
|
+
if (!isInsideGitRepo(projectDir)) return "";
|
|
1130
|
+
return '\nNOTE: this project is inside a git repository. The management credential in .sakupa/site.json is the key to this site \u2014 do NOT commit it to a PUBLIC repository (add ".sakupa/" to .gitignore yourself if you want to keep it out of version control).';
|
|
1131
|
+
}
|
|
1103
1132
|
|
|
1104
1133
|
// src/version.ts
|
|
1105
|
-
var MCP_VERSION =
|
|
1134
|
+
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
1106
1135
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
1107
1136
|
|
|
1108
1137
|
// src/tools/context.ts
|
|
@@ -1270,6 +1299,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1270
1299
|
outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1271
1300
|
spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
|
|
1272
1301
|
spaFallbackConfirmed: z.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
|
|
1302
|
+
publicConfirmed: z.boolean().optional().describe(
|
|
1303
|
+
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1304
|
+
),
|
|
1273
1305
|
lang: z.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1274
1306
|
}
|
|
1275
1307
|
},
|
|
@@ -1290,6 +1322,11 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1290
1322
|
const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1291
1323
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1292
1324
|
const existing = readSiteFile(ctx.projectDir);
|
|
1325
|
+
if (!existing && args.publicConfirmed !== true) {
|
|
1326
|
+
return text(
|
|
1327
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1293
1330
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1294
1331
|
if (!existing) {
|
|
1295
1332
|
const created = await ctx.client.createSite({
|
|
@@ -1318,7 +1355,7 @@ Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
|
1318
1355
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1319
1356
|
` : "") + `
|
|
1320
1357
|
This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh_site extends the validity; subscribing the site (subscribe_site) makes this URL permanent. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
|
|
1321
|
-
` + (finalized2.warnings.length > 0 ? `
|
|
1358
|
+
` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
|
|
1322
1359
|
Warnings:
|
|
1323
1360
|
${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
1324
1361
|
);
|
|
@@ -1449,9 +1486,11 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1449
1486
|
server.registerTool(
|
|
1450
1487
|
"bind_domain",
|
|
1451
1488
|
{
|
|
1452
|
-
description: "Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent {shortId}.sakupa.com URL keeps working alongside it. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the
|
|
1489
|
+
description: "Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent {shortId}.sakupa.com URL keeps working alongside it. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership. A binding request never reserves the domain: whoever proves DNS control first gets it, and unverified requests expire after 72 hours. Call again with verificationId to check progress.",
|
|
1453
1490
|
inputSchema: {
|
|
1454
|
-
hostname: z.string().describe(
|
|
1491
|
+
hostname: z.string().describe(
|
|
1492
|
+
'Domain to bind: the apex ("example.com") or its www form ("www.example.com") \u2014 both mean the same unit. Other subdomains cannot be bound.'
|
|
1493
|
+
),
|
|
1455
1494
|
verificationId: z.string().optional().describe("Check an existing DNS verification instead of starting a new one.")
|
|
1456
1495
|
}
|
|
1457
1496
|
},
|
|
@@ -1466,7 +1505,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1466
1505
|
return text(
|
|
1467
1506
|
`DNS verification ${res2.verificationId}: ${res2.status}
|
|
1468
1507
|
${res2.message}
|
|
1469
|
-
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS
|
|
1508
|
+
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificates and serving setup are in progress; check again with bind_domain + verificationId later.
|
|
1470
1509
|
` : "") + (res2.pendingDnsRecords.length > 0 ? `
|
|
1471
1510
|
DNS records still required:
|
|
1472
1511
|
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
@@ -1477,18 +1516,16 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
|
1477
1516
|
hostname: args.hostname
|
|
1478
1517
|
};
|
|
1479
1518
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
1480
|
-
const servingHint = res.isApex ? `"${res.hostname}" is an apex domain: your DNS provider must support ALIAS, ANAME, CNAME flattening, or alias records to point it at the assigned CDN domain.` : `"${res.hostname}" is a subdomain: create a CNAME record pointing it at the assigned CDN domain once provisioning completes.`;
|
|
1481
1519
|
return text(
|
|
1482
|
-
`Domain binding started for ${res.
|
|
1520
|
+
`Domain binding started for ${res.apexDomain} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
1483
1521
|
|
|
1484
1522
|
1. Prove control of ${res.apexDomain} by creating this DNS record:
|
|
1485
1523
|
name: ${res.verificationRecord.name}
|
|
1486
1524
|
type: ${res.verificationRecord.type}
|
|
1487
1525
|
value: ${res.verificationRecord.value}
|
|
1488
|
-
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the
|
|
1526
|
+
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the domain \u2014 the first verified request wins, and this challenge expires after 72 hours.
|
|
1489
1527
|
|
|
1490
|
-
2. Serving DNS (after verification): ${
|
|
1491
|
-
Server guidance: ${res.servingInstructions}
|
|
1528
|
+
2. Serving DNS (after verification): ${res.servingInstructions}
|
|
1492
1529
|
|
|
1493
1530
|
Then run bind_domain again with verificationId: "${res.verificationId}" to check verification and start provisioning.`
|
|
1494
1531
|
);
|
|
@@ -1530,18 +1567,40 @@ Full status:`, res);
|
|
|
1530
1567
|
server.registerTool(
|
|
1531
1568
|
"manage_billing",
|
|
1532
1569
|
{
|
|
1533
|
-
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.
|
|
1534
|
-
inputSchema: {
|
|
1570
|
+
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. If the local credential was lost, pass the remembered Sakupa site URL: Sakupa returns the same generic response for every URL and, only when an eligible subscription exists, emails its Stripe billing address a one-time period-end cancellation confirmation link. This never restores site authority.",
|
|
1571
|
+
inputSchema: {
|
|
1572
|
+
siteUrl: z.string().optional().describe(
|
|
1573
|
+
"Without .sakupa/site.json only: the remembered https://{shortId}.sakupa.com URL."
|
|
1574
|
+
)
|
|
1575
|
+
}
|
|
1535
1576
|
},
|
|
1536
|
-
async () => {
|
|
1577
|
+
async (args) => {
|
|
1537
1578
|
try {
|
|
1538
|
-
const site =
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1579
|
+
const site = readSiteFile(ctx.projectDir);
|
|
1580
|
+
if (site) {
|
|
1581
|
+
if (args.siteUrl !== void 0) {
|
|
1582
|
+
return text(
|
|
1583
|
+
"A local Sakupa credential is present, so manage_billing uses the site-specific Stripe portal. Remove siteUrl and call manage_billing again; the public lost-key path is deliberately unavailable while site authority is present."
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
1587
|
+
return text(
|
|
1588
|
+
`Stripe billing portal for this site:
|
|
1589
|
+
${res2.portalUrl}
|
|
1543
1590
|
|
|
1544
1591
|
Open this link in a browser to update the payment method, view invoices, or manage the subscription. The link is temporary \u2014 create a fresh one when needed.`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
if (args.siteUrl === void 0) {
|
|
1595
|
+
return text(
|
|
1596
|
+
"No .sakupa/site.json was found. Provide the remembered Sakupa site URL to request a cancellation email. Sakupa will not reveal whether the URL, site, billing email, or subscription exists. If eligible, the original Stripe billing email receives a short-lived one-time link. Opening it only shows the consequences; the user must press the confirmation button to stop the next renewal. This does not recover a key or grant deployment, download, or content access."
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
const res = await ctx.client.requestBillingCancellation({ siteUrl: args.siteUrl });
|
|
1600
|
+
return text(
|
|
1601
|
+
`${res.message}
|
|
1602
|
+
|
|
1603
|
+
For privacy, this response is identical whether or not the URL, site, customer, email, or subscription exists. Check the original Stripe billing inbox. The email link expires quickly and can only schedule cancellation at the current paid period end; it cannot restore the Sakupa credential or access the site.`
|
|
1545
1604
|
);
|
|
1546
1605
|
} catch (e) {
|
|
1547
1606
|
return toolError(e);
|
|
@@ -1593,7 +1652,7 @@ Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
|
1593
1652
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
1594
1653
|
|
|
1595
1654
|
A NEW management credential was written to .sakupa/site.json in this project \u2014 this project now manages the site.
|
|
1596
|
-
|
|
1655
|
+
` + credentialGitReminder(ctx.projectDir) + `
|
|
1597
1656
|
Download the current site content (signed URL):
|
|
1598
1657
|
${res.archiveUrl}`
|
|
1599
1658
|
);
|
|
@@ -1605,13 +1664,12 @@ ${res.archiveUrl}`
|
|
|
1605
1664
|
server.registerTool(
|
|
1606
1665
|
"set_billing_plan",
|
|
1607
1666
|
{
|
|
1608
|
-
description: `Change this site's hosting plan
|
|
1667
|
+
description: `Change this site's hosting plan or cancel/re-enable renewal. Plans: ${planCatalog()}. Plan changes go through the Stripe subscription; renewals bill the new plan. Auto-upgrade (one plan up when usage exceeds the current plan, capped at Business) is built in and not configurable. cancelRenewal: true ends the subscription at the period end; the paid service remains available until then. The site reverts to a free 24h site only after the signed Stripe final-cancellation webhook arrives. The API returns the consequences first; explicit owner confirmation (confirm: true) is required before anything is applied.`,
|
|
1609
1668
|
inputSchema: {
|
|
1610
1669
|
plan: planEnum.optional().describe("Target monthly plan."),
|
|
1611
1670
|
cancelRenewal: z.boolean().optional().describe(
|
|
1612
1671
|
"true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
|
|
1613
1672
|
),
|
|
1614
|
-
cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
|
|
1615
1673
|
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1616
1674
|
}
|
|
1617
1675
|
},
|
|
@@ -1621,7 +1679,6 @@ ${res.archiveUrl}`
|
|
|
1621
1679
|
const req = {
|
|
1622
1680
|
...args.plan !== void 0 ? { plan: args.plan } : {},
|
|
1623
1681
|
...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
|
|
1624
|
-
...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
|
|
1625
1682
|
confirm: args.confirm === true
|
|
1626
1683
|
};
|
|
1627
1684
|
try {
|
|
@@ -1802,8 +1859,8 @@ Workflow:
|
|
|
1802
1859
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
1803
1860
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
1804
1861
|
set_billing_plan, manage_billing and recover_domain_site manage the paid lifecycle.
|
|
1805
|
-
|
|
1806
|
-
|
|
1862
|
+
An immediate Stripe cancellation reverts the site to a free 24h site and removes all paid
|
|
1863
|
+
data when Sakupa receives the signed cancellation webhook.
|
|
1807
1864
|
|
|
1808
1865
|
Safety boundaries:
|
|
1809
1866
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
@@ -1811,7 +1868,9 @@ Safety boundaries:
|
|
|
1811
1868
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
1812
1869
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
1813
1870
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
1814
|
-
a bound custom domain, a lost credential is unrecoverable by design
|
|
1871
|
+
a bound custom domain, a lost credential is unrecoverable by design. manage_billing can accept
|
|
1872
|
+
the remembered Sakupa URL and request a one-time cancellation link sent only to the exact
|
|
1873
|
+
subscription's Stripe billing email; it never restores site authority.`;
|
|
1815
1874
|
function createSakupaMcpServer(opts) {
|
|
1816
1875
|
const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
|
|
1817
1876
|
const server = new McpServer(
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
// src/version.ts
|
|
2
|
-
var MCP_VERSION = "0.3.0";
|
|
3
|
-
var CLIENT_TYPE = "sakupa-mcp";
|
|
4
|
-
|
|
5
1
|
// ../core/dist/domain/constants.js
|
|
6
2
|
var SERVICE_DOMAIN = "sakupa.com";
|
|
7
3
|
var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
@@ -124,6 +120,9 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
120
|
];
|
|
125
121
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
126
122
|
|
|
123
|
+
// ../core/dist/domain/version.js
|
|
124
|
+
var SAKUPA_MCP_VERSION = "0.5.0";
|
|
125
|
+
|
|
127
126
|
// ../core/dist/domain/errors.js
|
|
128
127
|
var HTTP_STATUS = {
|
|
129
128
|
invalid_request: 400,
|
|
@@ -459,6 +458,16 @@ var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
|
459
458
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
460
459
|
var utf8Encoder = new TextEncoder();
|
|
461
460
|
|
|
461
|
+
// ../core/dist/services/subscriptions.js
|
|
462
|
+
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
463
|
+
|
|
464
|
+
// ../core/dist/services/billing-cancellation.js
|
|
465
|
+
var encoder = new TextEncoder();
|
|
466
|
+
|
|
467
|
+
// src/version.ts
|
|
468
|
+
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
469
|
+
var CLIENT_TYPE = "sakupa-mcp";
|
|
470
|
+
|
|
462
471
|
// src/transport.ts
|
|
463
472
|
var FetchTransport = class {
|
|
464
473
|
baseUrl;
|
|
@@ -657,6 +666,13 @@ var HttpApiClient = class {
|
|
|
657
666
|
{ credential, body: req }
|
|
658
667
|
);
|
|
659
668
|
}
|
|
669
|
+
async requestBillingCancellation(req) {
|
|
670
|
+
return this.call(
|
|
671
|
+
"POST",
|
|
672
|
+
"/v1/billing/cancellation-requests",
|
|
673
|
+
{ body: req }
|
|
674
|
+
);
|
|
675
|
+
}
|
|
660
676
|
async createTicket(credential, req) {
|
|
661
677
|
return this.call("POST", "/v1/support/tickets", {
|
|
662
678
|
credential,
|
|
@@ -673,7 +689,7 @@ var HttpApiClient = class {
|
|
|
673
689
|
|
|
674
690
|
// src/project-file.ts
|
|
675
691
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
676
|
-
import { join } from "node:path";
|
|
692
|
+
import { dirname, join } from "node:path";
|
|
677
693
|
var SITE_DIR = ".sakupa";
|
|
678
694
|
var SITE_FILE = "site.json";
|
|
679
695
|
function siteFilePath(projectDir) {
|
|
@@ -722,6 +738,19 @@ function deleteSiteFile(projectDir) {
|
|
|
722
738
|
rmSync(path, { force: true });
|
|
723
739
|
}
|
|
724
740
|
}
|
|
741
|
+
function isInsideGitRepo(projectDir) {
|
|
742
|
+
let dir = projectDir;
|
|
743
|
+
for (; ; ) {
|
|
744
|
+
if (existsSync(join(dir, ".git"))) return true;
|
|
745
|
+
const parent = dirname(dir);
|
|
746
|
+
if (parent === dir) return false;
|
|
747
|
+
dir = parent;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function credentialGitReminder(projectDir) {
|
|
751
|
+
if (!isInsideGitRepo(projectDir)) return "";
|
|
752
|
+
return '\nNOTE: this project is inside a git repository. The management credential in .sakupa/site.json is the key to this site \u2014 do NOT commit it to a PUBLIC repository (add ".sakupa/" to .gitignore yourself if you want to keep it out of version control).';
|
|
753
|
+
}
|
|
725
754
|
|
|
726
755
|
// src/analyze/analyzer.ts
|
|
727
756
|
import { promises as fs } from "node:fs";
|
|
@@ -1318,6 +1347,9 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1318
1347
|
outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1319
1348
|
spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
|
|
1320
1349
|
spaFallbackConfirmed: z.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
|
|
1350
|
+
publicConfirmed: z.boolean().optional().describe(
|
|
1351
|
+
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1352
|
+
),
|
|
1321
1353
|
lang: z.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1322
1354
|
}
|
|
1323
1355
|
},
|
|
@@ -1338,6 +1370,11 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1338
1370
|
const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1339
1371
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1340
1372
|
const existing = readSiteFile(ctx.projectDir);
|
|
1373
|
+
if (!existing && args.publicConfirmed !== true) {
|
|
1374
|
+
return text(
|
|
1375
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1341
1378
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1342
1379
|
if (!existing) {
|
|
1343
1380
|
const created = await ctx.client.createSite({
|
|
@@ -1366,7 +1403,7 @@ Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
|
1366
1403
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1367
1404
|
` : "") + `
|
|
1368
1405
|
This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh_site extends the validity; subscribing the site (subscribe_site) makes this URL permanent. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
|
|
1369
|
-
` + (finalized2.warnings.length > 0 ? `
|
|
1406
|
+
` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
|
|
1370
1407
|
Warnings:
|
|
1371
1408
|
${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
1372
1409
|
);
|
|
@@ -1497,9 +1534,11 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1497
1534
|
server.registerTool(
|
|
1498
1535
|
"bind_domain",
|
|
1499
1536
|
{
|
|
1500
|
-
description: "Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent {shortId}.sakupa.com URL keeps working alongside it. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the
|
|
1537
|
+
description: "Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent {shortId}.sakupa.com URL keeps working alongside it. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership. A binding request never reserves the domain: whoever proves DNS control first gets it, and unverified requests expire after 72 hours. Call again with verificationId to check progress.",
|
|
1501
1538
|
inputSchema: {
|
|
1502
|
-
hostname: z.string().describe(
|
|
1539
|
+
hostname: z.string().describe(
|
|
1540
|
+
'Domain to bind: the apex ("example.com") or its www form ("www.example.com") \u2014 both mean the same unit. Other subdomains cannot be bound.'
|
|
1541
|
+
),
|
|
1503
1542
|
verificationId: z.string().optional().describe("Check an existing DNS verification instead of starting a new one.")
|
|
1504
1543
|
}
|
|
1505
1544
|
},
|
|
@@ -1514,7 +1553,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1514
1553
|
return text(
|
|
1515
1554
|
`DNS verification ${res2.verificationId}: ${res2.status}
|
|
1516
1555
|
${res2.message}
|
|
1517
|
-
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS
|
|
1556
|
+
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificates and serving setup are in progress; check again with bind_domain + verificationId later.
|
|
1518
1557
|
` : "") + (res2.pendingDnsRecords.length > 0 ? `
|
|
1519
1558
|
DNS records still required:
|
|
1520
1559
|
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
@@ -1525,18 +1564,16 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
|
1525
1564
|
hostname: args.hostname
|
|
1526
1565
|
};
|
|
1527
1566
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
1528
|
-
const servingHint = res.isApex ? `"${res.hostname}" is an apex domain: your DNS provider must support ALIAS, ANAME, CNAME flattening, or alias records to point it at the assigned CDN domain.` : `"${res.hostname}" is a subdomain: create a CNAME record pointing it at the assigned CDN domain once provisioning completes.`;
|
|
1529
1567
|
return text(
|
|
1530
|
-
`Domain binding started for ${res.
|
|
1568
|
+
`Domain binding started for ${res.apexDomain} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
1531
1569
|
|
|
1532
1570
|
1. Prove control of ${res.apexDomain} by creating this DNS record:
|
|
1533
1571
|
name: ${res.verificationRecord.name}
|
|
1534
1572
|
type: ${res.verificationRecord.type}
|
|
1535
1573
|
value: ${res.verificationRecord.value}
|
|
1536
|
-
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the
|
|
1574
|
+
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the domain \u2014 the first verified request wins, and this challenge expires after 72 hours.
|
|
1537
1575
|
|
|
1538
|
-
2. Serving DNS (after verification): ${
|
|
1539
|
-
Server guidance: ${res.servingInstructions}
|
|
1576
|
+
2. Serving DNS (after verification): ${res.servingInstructions}
|
|
1540
1577
|
|
|
1541
1578
|
Then run bind_domain again with verificationId: "${res.verificationId}" to check verification and start provisioning.`
|
|
1542
1579
|
);
|
|
@@ -1578,18 +1615,40 @@ Full status:`, res);
|
|
|
1578
1615
|
server.registerTool(
|
|
1579
1616
|
"manage_billing",
|
|
1580
1617
|
{
|
|
1581
|
-
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.
|
|
1582
|
-
inputSchema: {
|
|
1618
|
+
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. If the local credential was lost, pass the remembered Sakupa site URL: Sakupa returns the same generic response for every URL and, only when an eligible subscription exists, emails its Stripe billing address a one-time period-end cancellation confirmation link. This never restores site authority.",
|
|
1619
|
+
inputSchema: {
|
|
1620
|
+
siteUrl: z.string().optional().describe(
|
|
1621
|
+
"Without .sakupa/site.json only: the remembered https://{shortId}.sakupa.com URL."
|
|
1622
|
+
)
|
|
1623
|
+
}
|
|
1583
1624
|
},
|
|
1584
|
-
async () => {
|
|
1625
|
+
async (args) => {
|
|
1585
1626
|
try {
|
|
1586
|
-
const site =
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1627
|
+
const site = readSiteFile(ctx.projectDir);
|
|
1628
|
+
if (site) {
|
|
1629
|
+
if (args.siteUrl !== void 0) {
|
|
1630
|
+
return text(
|
|
1631
|
+
"A local Sakupa credential is present, so manage_billing uses the site-specific Stripe portal. Remove siteUrl and call manage_billing again; the public lost-key path is deliberately unavailable while site authority is present."
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
1635
|
+
return text(
|
|
1636
|
+
`Stripe billing portal for this site:
|
|
1637
|
+
${res2.portalUrl}
|
|
1591
1638
|
|
|
1592
1639
|
Open this link in a browser to update the payment method, view invoices, or manage the subscription. The link is temporary \u2014 create a fresh one when needed.`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
if (args.siteUrl === void 0) {
|
|
1643
|
+
return text(
|
|
1644
|
+
"No .sakupa/site.json was found. Provide the remembered Sakupa site URL to request a cancellation email. Sakupa will not reveal whether the URL, site, billing email, or subscription exists. If eligible, the original Stripe billing email receives a short-lived one-time link. Opening it only shows the consequences; the user must press the confirmation button to stop the next renewal. This does not recover a key or grant deployment, download, or content access."
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
const res = await ctx.client.requestBillingCancellation({ siteUrl: args.siteUrl });
|
|
1648
|
+
return text(
|
|
1649
|
+
`${res.message}
|
|
1650
|
+
|
|
1651
|
+
For privacy, this response is identical whether or not the URL, site, customer, email, or subscription exists. Check the original Stripe billing inbox. The email link expires quickly and can only schedule cancellation at the current paid period end; it cannot restore the Sakupa credential or access the site.`
|
|
1593
1652
|
);
|
|
1594
1653
|
} catch (e) {
|
|
1595
1654
|
return toolError(e);
|
|
@@ -1641,7 +1700,7 @@ Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
|
1641
1700
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
1642
1701
|
|
|
1643
1702
|
A NEW management credential was written to .sakupa/site.json in this project \u2014 this project now manages the site.
|
|
1644
|
-
|
|
1703
|
+
` + credentialGitReminder(ctx.projectDir) + `
|
|
1645
1704
|
Download the current site content (signed URL):
|
|
1646
1705
|
${res.archiveUrl}`
|
|
1647
1706
|
);
|
|
@@ -1653,13 +1712,12 @@ ${res.archiveUrl}`
|
|
|
1653
1712
|
server.registerTool(
|
|
1654
1713
|
"set_billing_plan",
|
|
1655
1714
|
{
|
|
1656
|
-
description: `Change this site's hosting plan
|
|
1715
|
+
description: `Change this site's hosting plan or cancel/re-enable renewal. Plans: ${planCatalog()}. Plan changes go through the Stripe subscription; renewals bill the new plan. Auto-upgrade (one plan up when usage exceeds the current plan, capped at Business) is built in and not configurable. cancelRenewal: true ends the subscription at the period end; the paid service remains available until then. The site reverts to a free 24h site only after the signed Stripe final-cancellation webhook arrives. The API returns the consequences first; explicit owner confirmation (confirm: true) is required before anything is applied.`,
|
|
1657
1716
|
inputSchema: {
|
|
1658
1717
|
plan: planEnum.optional().describe("Target monthly plan."),
|
|
1659
1718
|
cancelRenewal: z.boolean().optional().describe(
|
|
1660
1719
|
"true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
|
|
1661
1720
|
),
|
|
1662
|
-
cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
|
|
1663
1721
|
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1664
1722
|
}
|
|
1665
1723
|
},
|
|
@@ -1669,7 +1727,6 @@ ${res.archiveUrl}`
|
|
|
1669
1727
|
const req = {
|
|
1670
1728
|
...args.plan !== void 0 ? { plan: args.plan } : {},
|
|
1671
1729
|
...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
|
|
1672
|
-
...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
|
|
1673
1730
|
confirm: args.confirm === true
|
|
1674
1731
|
};
|
|
1675
1732
|
try {
|
|
@@ -1798,8 +1855,8 @@ Workflow:
|
|
|
1798
1855
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
1799
1856
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
1800
1857
|
set_billing_plan, manage_billing and recover_domain_site manage the paid lifecycle.
|
|
1801
|
-
|
|
1802
|
-
|
|
1858
|
+
An immediate Stripe cancellation reverts the site to a free 24h site and removes all paid
|
|
1859
|
+
data when Sakupa receives the signed cancellation webhook.
|
|
1803
1860
|
|
|
1804
1861
|
Safety boundaries:
|
|
1805
1862
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
@@ -1807,7 +1864,9 @@ Safety boundaries:
|
|
|
1807
1864
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
1808
1865
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
1809
1866
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
1810
|
-
a bound custom domain, a lost credential is unrecoverable by design
|
|
1867
|
+
a bound custom domain, a lost credential is unrecoverable by design. manage_billing can accept
|
|
1868
|
+
the remembered Sakupa URL and request a one-time cancellation link sent only to the exact
|
|
1869
|
+
subscription's Stripe billing email; it never restores site authority.`;
|
|
1811
1870
|
function createSakupaMcpServer(opts) {
|
|
1812
1871
|
const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
|
|
1813
1872
|
const server = new McpServer(
|