@sakupa/mcp 0.4.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.
Files changed (3) hide show
  1. package/dist/bin.js +61 -15
  2. package/dist/index.js +64 -18
  3. 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,
@@ -1115,7 +1131,7 @@ function credentialGitReminder(projectDir) {
1115
1131
  }
1116
1132
 
1117
1133
  // src/version.ts
1118
- var MCP_VERSION = "0.4.0";
1134
+ var MCP_VERSION = SAKUPA_MCP_VERSION;
1119
1135
  var CLIENT_TYPE = "sakupa-mcp";
1120
1136
 
1121
1137
  // src/tools/context.ts
@@ -1283,6 +1299,9 @@ Next action: ${analysis.suggestedNextAction}`,
1283
1299
  outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1284
1300
  spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
1285
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
+ ),
1286
1305
  lang: z.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
1287
1306
  }
1288
1307
  },
@@ -1303,6 +1322,11 @@ Next action: ${analysis.suggestedNextAction}`,
1303
1322
  const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
1304
1323
  const manifest = await buildHashedManifest(files, outputAbs);
1305
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
+ }
1306
1330
  ensureUploadSizeWithinLimits(manifest, !existing);
1307
1331
  if (!existing) {
1308
1332
  const created = await ctx.client.createSite({
@@ -1543,18 +1567,40 @@ Full status:`, res);
1543
1567
  server.registerTool(
1544
1568
  "manage_billing",
1545
1569
  {
1546
- 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. Requires the local site credential (.sakupa/site.json).",
1547
- 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
+ }
1548
1576
  },
1549
- async () => {
1577
+ async (args) => {
1550
1578
  try {
1551
- const site = requireSiteFile(ctx);
1552
- const res = await ctx.client.createBillingPortal(site.siteId, site.credential);
1553
- return text(
1554
- `Stripe billing portal for this site:
1555
- ${res.portalUrl}
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}
1556
1590
 
1557
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.`
1558
1604
  );
1559
1605
  } catch (e) {
1560
1606
  return toolError(e);
@@ -1618,13 +1664,12 @@ ${res.archiveUrl}`
1618
1664
  server.registerTool(
1619
1665
  "set_billing_plan",
1620
1666
  {
1621
- description: `Change this site's hosting plan, cancel/re-enable renewal, or cancel immediately. 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; cancelNow: true ends it IMMEDIATELY \u2014 the site reverts to a free 24h site at once and all paid data (custom domain bindings included) is removed, with no refund of the current period. The API returns the consequences first; explicit owner confirmation (confirm: true) is required before anything is applied.`,
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.`,
1622
1668
  inputSchema: {
1623
1669
  plan: planEnum.optional().describe("Target monthly plan."),
1624
1670
  cancelRenewal: z.boolean().optional().describe(
1625
1671
  "true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
1626
1672
  ),
1627
- cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
1628
1673
  confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
1629
1674
  }
1630
1675
  },
@@ -1634,7 +1679,6 @@ ${res.archiveUrl}`
1634
1679
  const req = {
1635
1680
  ...args.plan !== void 0 ? { plan: args.plan } : {},
1636
1681
  ...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
1637
- ...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
1638
1682
  confirm: args.confirm === true
1639
1683
  };
1640
1684
  try {
@@ -1815,8 +1859,8 @@ Workflow:
1815
1859
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
1816
1860
  first verified request wins; unverified requests expire after 72 hours. billing_status,
1817
1861
  set_billing_plan, manage_billing and recover_domain_site manage the paid lifecycle.
1818
- Canceling the subscription immediately reverts the site to a free 24h site and removes all
1819
- paid data.
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.
1820
1864
 
1821
1865
  Safety boundaries:
1822
1866
  - Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
@@ -1824,7 +1868,9 @@ Safety boundaries:
1824
1868
  - Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
1825
1869
  - A subscription never grants domain ownership; only DNS verification does.
1826
1870
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
1827
- 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.`;
1828
1874
  function createSakupaMcpServer(opts) {
1829
1875
  const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
1830
1876
  const server = new McpServer(
package/dist/index.js CHANGED
@@ -1,7 +1,3 @@
1
- // src/version.ts
2
- var MCP_VERSION = "0.4.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,
@@ -1331,6 +1347,9 @@ Next action: ${analysis.suggestedNextAction}`,
1331
1347
  outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
1332
1348
  spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
1333
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
+ ),
1334
1353
  lang: z.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
1335
1354
  }
1336
1355
  },
@@ -1351,6 +1370,11 @@ Next action: ${analysis.suggestedNextAction}`,
1351
1370
  const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
1352
1371
  const manifest = await buildHashedManifest(files, outputAbs);
1353
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
+ }
1354
1378
  ensureUploadSizeWithinLimits(manifest, !existing);
1355
1379
  if (!existing) {
1356
1380
  const created = await ctx.client.createSite({
@@ -1591,18 +1615,40 @@ Full status:`, res);
1591
1615
  server.registerTool(
1592
1616
  "manage_billing",
1593
1617
  {
1594
- 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. Requires the local site credential (.sakupa/site.json).",
1595
- 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
+ }
1596
1624
  },
1597
- async () => {
1625
+ async (args) => {
1598
1626
  try {
1599
- const site = requireSiteFile(ctx);
1600
- const res = await ctx.client.createBillingPortal(site.siteId, site.credential);
1601
- return text(
1602
- `Stripe billing portal for this site:
1603
- ${res.portalUrl}
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}
1604
1638
 
1605
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.`
1606
1652
  );
1607
1653
  } catch (e) {
1608
1654
  return toolError(e);
@@ -1666,13 +1712,12 @@ ${res.archiveUrl}`
1666
1712
  server.registerTool(
1667
1713
  "set_billing_plan",
1668
1714
  {
1669
- description: `Change this site's hosting plan, cancel/re-enable renewal, or cancel immediately. 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; cancelNow: true ends it IMMEDIATELY \u2014 the site reverts to a free 24h site at once and all paid data (custom domain bindings included) is removed, with no refund of the current period. The API returns the consequences first; explicit owner confirmation (confirm: true) is required before anything is applied.`,
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.`,
1670
1716
  inputSchema: {
1671
1717
  plan: planEnum.optional().describe("Target monthly plan."),
1672
1718
  cancelRenewal: z.boolean().optional().describe(
1673
1719
  "true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
1674
1720
  ),
1675
- cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
1676
1721
  confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
1677
1722
  }
1678
1723
  },
@@ -1682,7 +1727,6 @@ ${res.archiveUrl}`
1682
1727
  const req = {
1683
1728
  ...args.plan !== void 0 ? { plan: args.plan } : {},
1684
1729
  ...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
1685
- ...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
1686
1730
  confirm: args.confirm === true
1687
1731
  };
1688
1732
  try {
@@ -1811,8 +1855,8 @@ Workflow:
1811
1855
  serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
1812
1856
  first verified request wins; unverified requests expire after 72 hours. billing_status,
1813
1857
  set_billing_plan, manage_billing and recover_domain_site manage the paid lifecycle.
1814
- Canceling the subscription immediately reverts the site to a free 24h site and removes all
1815
- paid data.
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.
1816
1860
 
1817
1861
  Safety boundaries:
1818
1862
  - Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
@@ -1820,7 +1864,9 @@ Safety boundaries:
1820
1864
  - Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
1821
1865
  - A subscription never grants domain ownership; only DNS verification does.
1822
1866
  - The management credential lives only in .sakupa/site.json; never share or upload it. Without
1823
- 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.`;
1824
1870
  function createSakupaMcpServer(opts) {
1825
1871
  const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
1826
1872
  const server = new McpServer(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",