@sakupa/mcp 0.7.7 → 0.7.8
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 +246 -86
- package/dist/index.js +247 -85
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
|
5
5
|
var TEST_ACCESS_HEADER = "x-sakupa-test-token";
|
|
6
6
|
var FREE_SITE_TTL_HOURS = 24;
|
|
7
7
|
var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
var FREE_ACTIVE_SITES_PER_IP = 3;
|
|
8
9
|
var PAID_SITE_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
9
10
|
var MAX_FILE_COUNT = 5e3;
|
|
10
11
|
var MAX_SINGLE_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -123,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
123
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
124
125
|
|
|
125
126
|
// ../core/dist/domain/version.js
|
|
126
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.8";
|
|
127
128
|
|
|
128
129
|
// ../core/dist/domain/errors.js
|
|
129
130
|
var HTTP_STATUS = {
|
|
@@ -1265,6 +1266,12 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1265
1266
|
};
|
|
1266
1267
|
}
|
|
1267
1268
|
|
|
1269
|
+
// src/tools/context.ts
|
|
1270
|
+
import { z as z2 } from "zod";
|
|
1271
|
+
import { statSync } from "node:fs";
|
|
1272
|
+
import { homedir } from "node:os";
|
|
1273
|
+
import { isAbsolute, parse, resolve as resolve2 } from "node:path";
|
|
1274
|
+
|
|
1268
1275
|
// src/tools/result.ts
|
|
1269
1276
|
import { z } from "zod";
|
|
1270
1277
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
@@ -1307,22 +1314,64 @@ function structuredToolResult(envelope) {
|
|
|
1307
1314
|
}
|
|
1308
1315
|
|
|
1309
1316
|
// src/tools/context.ts
|
|
1317
|
+
var LocalGuidanceError = class extends SakupaError {
|
|
1318
|
+
constructor(code, message) {
|
|
1319
|
+
super(code, message);
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
var projectDirInput = z2.string().optional().describe(
|
|
1323
|
+
"Absolute path of the project directory the user is CURRENTLY working in (the folder holding the site files and .sakupa). Always pass it explicitly; when omitted the server falls back to its startup directory, which may not be where the user is working now."
|
|
1324
|
+
);
|
|
1325
|
+
function withProjectDir(ctx, projectDirArg) {
|
|
1326
|
+
if (projectDirArg === void 0) return ctx;
|
|
1327
|
+
if (!isAbsolute(projectDirArg)) {
|
|
1328
|
+
throw new LocalGuidanceError(
|
|
1329
|
+
"invalid_request",
|
|
1330
|
+
`projectDir must be an ABSOLUTE path (got "${projectDirArg}"). Pass the full path of the directory the user is currently working in.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const dir = resolve2(projectDirArg);
|
|
1334
|
+
if (parse(dir).root === dir || dir === homedir()) {
|
|
1335
|
+
throw new LocalGuidanceError(
|
|
1336
|
+
"invalid_request",
|
|
1337
|
+
`projectDir "${dir}" is a filesystem root or the home directory. Pass the specific project folder that holds the site's files, not a top-level directory.`
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const stat = statSync(dir, { throwIfNoEntry: false });
|
|
1341
|
+
if (!stat?.isDirectory()) {
|
|
1342
|
+
throw new LocalGuidanceError(
|
|
1343
|
+
"invalid_request",
|
|
1344
|
+
`projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
return { ...ctx, projectDir: dir };
|
|
1348
|
+
}
|
|
1310
1349
|
function requireSiteFile(ctx) {
|
|
1311
1350
|
const state = loadSiteFile(ctx.projectDir);
|
|
1312
1351
|
if (state.kind === "corrupted") {
|
|
1313
|
-
throw new
|
|
1352
|
+
throw new LocalGuidanceError(
|
|
1314
1353
|
"invalid_request",
|
|
1315
1354
|
`.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
|
|
1316
1355
|
);
|
|
1317
1356
|
}
|
|
1318
1357
|
if (state.kind === "absent") {
|
|
1319
|
-
throw new
|
|
1358
|
+
throw new LocalGuidanceError(
|
|
1320
1359
|
"not_found",
|
|
1321
1360
|
`No .sakupa/site.json found in ${ctx.projectDir}. This project has no Sakupa site binding yet \u2014 run deploy_site first to publish it (the management credential will be stored locally in .sakupa/site.json). If this was a paid custom-domain site whose project file was lost, use recover_domain_site instead.`
|
|
1322
1361
|
);
|
|
1323
1362
|
}
|
|
1324
1363
|
return state.file;
|
|
1325
1364
|
}
|
|
1365
|
+
var STATIC_SUMMARY = {
|
|
1366
|
+
not_found: "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first.",
|
|
1367
|
+
unauthorized: "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.",
|
|
1368
|
+
invalid_request: "The request arguments or local project checks did not pass.",
|
|
1369
|
+
validation_failed: "The request arguments or local project checks did not pass.",
|
|
1370
|
+
state_conflict: "The resource state has changed; re-query the current status before deciding the next step.",
|
|
1371
|
+
confirmation_required: "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot.",
|
|
1372
|
+
payment_required: "This operation requires an active subscription; check billing_status first.",
|
|
1373
|
+
rate_limited: "The server rate limit was reached; retry after the returned wait time."
|
|
1374
|
+
};
|
|
1326
1375
|
function toolError(e) {
|
|
1327
1376
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1328
1377
|
const retryable = errorCode === "rate_limited" || errorCode === "internal";
|
|
@@ -1341,7 +1390,7 @@ function toolError(e) {
|
|
|
1341
1390
|
)
|
|
1342
1391
|
) : void 0;
|
|
1343
1392
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1344
|
-
const safeSummary = errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode
|
|
1393
|
+
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : STATIC_SUMMARY[errorCode] ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.");
|
|
1345
1394
|
const result = structuredToolResult({
|
|
1346
1395
|
schemaVersion: 1,
|
|
1347
1396
|
outcome: "failed",
|
|
@@ -1360,8 +1409,54 @@ function toolError(e) {
|
|
|
1360
1409
|
// src/tools/definitions.ts
|
|
1361
1410
|
import { randomUUID } from "node:crypto";
|
|
1362
1411
|
import { promises as fs2 } from "node:fs";
|
|
1363
|
-
import { join as
|
|
1364
|
-
import { z as
|
|
1412
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
1413
|
+
import { z as z3 } from "zod";
|
|
1414
|
+
|
|
1415
|
+
// src/creation-registry.ts
|
|
1416
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1417
|
+
import { homedir as homedir2 } from "node:os";
|
|
1418
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1419
|
+
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
1420
|
+
function creationRegistryPath() {
|
|
1421
|
+
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir2();
|
|
1422
|
+
return join3(base, ".sakupa", "created-sites.json");
|
|
1423
|
+
}
|
|
1424
|
+
function readAll() {
|
|
1425
|
+
const path = creationRegistryPath();
|
|
1426
|
+
if (!existsSync2(path)) return [];
|
|
1427
|
+
try {
|
|
1428
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1429
|
+
if (!Array.isArray(parsed)) return [];
|
|
1430
|
+
return parsed.filter(
|
|
1431
|
+
(e) => typeof e === "object" && e !== null && typeof e.siteId === "string" && typeof e.createdAt === "string"
|
|
1432
|
+
);
|
|
1433
|
+
} catch {
|
|
1434
|
+
return [];
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function writeAll(records) {
|
|
1438
|
+
const path = creationRegistryPath();
|
|
1439
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1440
|
+
writeFileSync2(path, `${JSON.stringify(records, null, 2)}
|
|
1441
|
+
`, "utf-8");
|
|
1442
|
+
}
|
|
1443
|
+
function listRecentCreations(nowMs) {
|
|
1444
|
+
return readAll().filter((e) => {
|
|
1445
|
+
const t = Date.parse(e.createdAt);
|
|
1446
|
+
return Number.isFinite(t) && nowMs - t < RECENT_WINDOW_MS;
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
function recordCreation(record) {
|
|
1450
|
+
const rest = readAll().filter((e) => e.siteId !== record.siteId);
|
|
1451
|
+
writeAll([...rest, record]);
|
|
1452
|
+
}
|
|
1453
|
+
function removeCreation(siteId) {
|
|
1454
|
+
const all = readAll();
|
|
1455
|
+
const rest = all.filter((e) => e.siteId !== siteId);
|
|
1456
|
+
if (rest.length !== all.length) writeAll(rest);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/tools/definitions.ts
|
|
1365
1460
|
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
1366
1461
|
return structuredToolResult({
|
|
1367
1462
|
schemaVersion: 1,
|
|
@@ -1384,12 +1479,12 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
1384
1479
|
nextActions: []
|
|
1385
1480
|
});
|
|
1386
1481
|
}
|
|
1387
|
-
var planEnum =
|
|
1388
|
-
var severityEnum =
|
|
1482
|
+
var planEnum = z3.enum(["water", "personal", "share", "business"]);
|
|
1483
|
+
var severityEnum = z3.enum(["low", "medium", "high", "critical"]);
|
|
1389
1484
|
function planCatalog() {
|
|
1390
1485
|
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
1391
1486
|
}
|
|
1392
|
-
var ticketCategoryEnum =
|
|
1487
|
+
var ticketCategoryEnum = z3.enum([
|
|
1393
1488
|
"billing",
|
|
1394
1489
|
"payment",
|
|
1395
1490
|
"refund_review",
|
|
@@ -1437,7 +1532,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1437
1532
|
async function buildHashedManifest(files, outputAbs) {
|
|
1438
1533
|
const manifest = [];
|
|
1439
1534
|
for (const file of files) {
|
|
1440
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1535
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, file.path)));
|
|
1441
1536
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1442
1537
|
}
|
|
1443
1538
|
return manifest;
|
|
@@ -1456,7 +1551,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1456
1551
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
1457
1552
|
);
|
|
1458
1553
|
}
|
|
1459
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1554
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, match.path)));
|
|
1460
1555
|
if (bytes.byteLength !== match.size) {
|
|
1461
1556
|
throw new SakupaError(
|
|
1462
1557
|
"validation_failed",
|
|
@@ -1467,8 +1562,23 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1467
1562
|
}
|
|
1468
1563
|
return targets.length;
|
|
1469
1564
|
}
|
|
1470
|
-
function
|
|
1471
|
-
const
|
|
1565
|
+
function freeSiteCreationBarrier() {
|
|
1566
|
+
const recent = listRecentCreations(Date.now());
|
|
1567
|
+
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
1568
|
+
const registryPath = creationRegistryPath();
|
|
1569
|
+
return text(
|
|
1570
|
+
"local_site_limit_reached",
|
|
1571
|
+
`This machine already created ${recent.length} free sites in the last 24 hours (the server also enforces ${FREE_ACTIVE_SITES_PER_IP} active free sites per IP). No new site was created.
|
|
1572
|
+
|
|
1573
|
+
` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
|
|
1574
|
+
|
|
1575
|
+
Options: delete one of these sites (run delete_site with its projectDir), wait for a free site to expire, or \u2014 if this list is stale because sites were deleted or subscribed elsewhere \u2014 remove the local registry file at ${registryPath} and retry.`,
|
|
1576
|
+
{ recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
|
|
1577
|
+
"blocked"
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
function registerTools(server, baseCtx) {
|
|
1581
|
+
const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
|
|
1472
1582
|
server.registerTool(
|
|
1473
1583
|
"analyze_site",
|
|
1474
1584
|
{
|
|
@@ -1476,11 +1586,13 @@ function registerTools(server, ctx) {
|
|
|
1476
1586
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1477
1587
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1478
1588
|
inputSchema: {
|
|
1479
|
-
|
|
1589
|
+
projectDir: projectDirInput,
|
|
1590
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1480
1591
|
}
|
|
1481
1592
|
},
|
|
1482
1593
|
async (args) => {
|
|
1483
1594
|
try {
|
|
1595
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1484
1596
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1485
1597
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1486
1598
|
});
|
|
@@ -1502,18 +1614,20 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1502
1614
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1503
1615
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1504
1616
|
inputSchema: {
|
|
1505
|
-
|
|
1506
|
-
|
|
1617
|
+
projectDir: projectDirInput,
|
|
1618
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1619
|
+
spaFallback: z3.boolean().optional().describe(
|
|
1507
1620
|
"Override automatic SPA-fallback detection (single index.html + JS auto-enables rewriting unknown paths to index.html; multiple HTML pages auto-disable it). Pass only to force the behavior against the detected structure."
|
|
1508
1621
|
),
|
|
1509
|
-
publicConfirmed:
|
|
1622
|
+
publicConfirmed: z3.boolean().optional().describe(
|
|
1510
1623
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1511
1624
|
),
|
|
1512
|
-
lang:
|
|
1625
|
+
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1513
1626
|
}
|
|
1514
1627
|
},
|
|
1515
1628
|
async (args) => {
|
|
1516
1629
|
try {
|
|
1630
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1517
1631
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1518
1632
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1519
1633
|
});
|
|
@@ -1521,7 +1635,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1521
1635
|
return notDeployableResult(analysis);
|
|
1522
1636
|
}
|
|
1523
1637
|
const files = analysis.files;
|
|
1524
|
-
const outputAbs =
|
|
1638
|
+
const outputAbs = resolve3(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1525
1639
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1526
1640
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1527
1641
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -1535,6 +1649,10 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1535
1649
|
);
|
|
1536
1650
|
}
|
|
1537
1651
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1652
|
+
if (!existing) {
|
|
1653
|
+
const barrier = freeSiteCreationBarrier();
|
|
1654
|
+
if (barrier) return barrier;
|
|
1655
|
+
}
|
|
1538
1656
|
if (!existing && args.publicConfirmed !== true) {
|
|
1539
1657
|
return text(
|
|
1540
1658
|
"public_deployment_confirmation_required",
|
|
@@ -1556,17 +1674,25 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1556
1674
|
created.siteId,
|
|
1557
1675
|
created.credential
|
|
1558
1676
|
);
|
|
1677
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1559
1678
|
writeSiteFile(ctx.projectDir, {
|
|
1560
1679
|
siteId: created.siteId,
|
|
1561
1680
|
shortId: created.shortId,
|
|
1562
1681
|
url: finalized2.url,
|
|
1563
1682
|
credential: created.credential,
|
|
1564
|
-
createdAt
|
|
1683
|
+
createdAt,
|
|
1565
1684
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1566
1685
|
});
|
|
1686
|
+
recordCreation({
|
|
1687
|
+
siteId: created.siteId,
|
|
1688
|
+
projectDir: ctx.projectDir,
|
|
1689
|
+
url: finalized2.url,
|
|
1690
|
+
createdAt
|
|
1691
|
+
});
|
|
1567
1692
|
return text(
|
|
1568
1693
|
"site_published",
|
|
1569
1694
|
`Site published: ${finalized2.url}
|
|
1695
|
+
Project directory: ${ctx.projectDir}
|
|
1570
1696
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1571
1697
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1572
1698
|
` : "") + `
|
|
@@ -1584,7 +1710,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1584
1710
|
filesUploaded: uploaded2,
|
|
1585
1711
|
totalBytes: finalized2.totalBytes,
|
|
1586
1712
|
warnings: finalized2.warnings,
|
|
1587
|
-
credentialStoredLocally: true
|
|
1713
|
+
credentialStoredLocally: true,
|
|
1714
|
+
projectDir: ctx.projectDir
|
|
1588
1715
|
}
|
|
1589
1716
|
);
|
|
1590
1717
|
}
|
|
@@ -1630,6 +1757,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1630
1757
|
return text(
|
|
1631
1758
|
"site_updated",
|
|
1632
1759
|
`Site updated: ${finalized.url}
|
|
1760
|
+
Project directory: ${ctx.projectDir}
|
|
1633
1761
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1634
1762
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1635
1763
|
` : "") + (finalized.mode === "free" ? `
|
|
@@ -1641,6 +1769,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1641
1769
|
siteId: existing.siteId,
|
|
1642
1770
|
url: finalized.url,
|
|
1643
1771
|
mode: finalized.mode,
|
|
1772
|
+
projectDir: ctx.projectDir,
|
|
1644
1773
|
expiresAt: finalized.expiresAt,
|
|
1645
1774
|
filesUploaded: uploaded,
|
|
1646
1775
|
totalBytes: finalized.totalBytes,
|
|
@@ -1658,17 +1787,18 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1658
1787
|
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.",
|
|
1659
1788
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1660
1789
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1661
|
-
inputSchema: {}
|
|
1790
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1662
1791
|
},
|
|
1663
|
-
async () => {
|
|
1792
|
+
async (args) => {
|
|
1664
1793
|
try {
|
|
1794
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1665
1795
|
const site = requireSiteFile(ctx);
|
|
1666
1796
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
1667
1797
|
return text(
|
|
1668
1798
|
"site_refreshed",
|
|
1669
|
-
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1799
|
+
`Site validity refreshed (project: ${ctx.projectDir}). New expiry: ${res.expiresAt}
|
|
1670
1800
|
NO content was uploaded or changed by this call \u2014 to publish new or edited files, run deploy_site. Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1671
|
-
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1801
|
+
{ siteId: site.siteId, expiresAt: res.expiresAt, projectDir: ctx.projectDir }
|
|
1672
1802
|
);
|
|
1673
1803
|
} catch (e) {
|
|
1674
1804
|
return toolError(e);
|
|
@@ -1681,13 +1811,17 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1681
1811
|
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.",
|
|
1682
1812
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1683
1813
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1684
|
-
inputSchema: {}
|
|
1814
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1685
1815
|
},
|
|
1686
|
-
async () => {
|
|
1816
|
+
async (args) => {
|
|
1687
1817
|
try {
|
|
1818
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1688
1819
|
const site = requireSiteFile(ctx);
|
|
1689
1820
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1690
|
-
return textJson("site_status_returned", "Site status:",
|
|
1821
|
+
return textJson("site_status_returned", "Site status:", {
|
|
1822
|
+
...res,
|
|
1823
|
+
projectDir: ctx.projectDir
|
|
1824
|
+
});
|
|
1691
1825
|
} catch (e) {
|
|
1692
1826
|
return toolError(e);
|
|
1693
1827
|
}
|
|
@@ -1700,6 +1834,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1700
1834
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1701
1835
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1702
1836
|
inputSchema: {
|
|
1837
|
+
projectDir: projectDirInput,
|
|
1703
1838
|
plan: planEnum.describe(
|
|
1704
1839
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1705
1840
|
)
|
|
@@ -1707,6 +1842,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1707
1842
|
},
|
|
1708
1843
|
async (args) => {
|
|
1709
1844
|
try {
|
|
1845
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1710
1846
|
const site = requireSiteFile(ctx);
|
|
1711
1847
|
const res = await ctx.client.createPlanCheckout(
|
|
1712
1848
|
{
|
|
@@ -1745,13 +1881,15 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1745
1881
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1746
1882
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1747
1883
|
inputSchema: {
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1884
|
+
projectDir: projectDirInput,
|
|
1885
|
+
action: z3.enum(["start", "status"]),
|
|
1886
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
1887
|
+
verificationId: z3.string().optional().describe("Required for status.")
|
|
1751
1888
|
}
|
|
1752
1889
|
},
|
|
1753
1890
|
async (args) => {
|
|
1754
1891
|
try {
|
|
1892
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1755
1893
|
const site = requireSiteFile(ctx);
|
|
1756
1894
|
if (args.action === "status") {
|
|
1757
1895
|
if (!args.verificationId) {
|
|
@@ -1821,10 +1959,11 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1821
1959
|
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).",
|
|
1822
1960
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1823
1961
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1824
|
-
inputSchema: {}
|
|
1962
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1825
1963
|
},
|
|
1826
|
-
async () => {
|
|
1964
|
+
async (args) => {
|
|
1827
1965
|
try {
|
|
1966
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1828
1967
|
const site = requireSiteFile(ctx);
|
|
1829
1968
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
1830
1969
|
const lines = [
|
|
@@ -1854,11 +1993,13 @@ Full status:`, res);
|
|
|
1854
1993
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1855
1994
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1856
1995
|
inputSchema: {
|
|
1857
|
-
|
|
1996
|
+
projectDir: projectDirInput,
|
|
1997
|
+
scope: z3.enum(["site", "public_recovery"])
|
|
1858
1998
|
}
|
|
1859
1999
|
},
|
|
1860
2000
|
async (args) => {
|
|
1861
2001
|
try {
|
|
2002
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1862
2003
|
if (args.scope === "site") {
|
|
1863
2004
|
const site = requireSiteFile(ctx);
|
|
1864
2005
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
@@ -1909,14 +2050,16 @@ Full status:`, res);
|
|
|
1909
2050
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1910
2051
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1911
2052
|
inputSchema: {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2053
|
+
projectDir: projectDirInput,
|
|
2054
|
+
action: z3.enum(["start", "status", "complete"]),
|
|
2055
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
2056
|
+
verificationId: z3.string().optional().describe("Required for status or complete."),
|
|
2057
|
+
preserveExistingCredentials: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
1916
2058
|
}
|
|
1917
2059
|
},
|
|
1918
2060
|
async (args) => {
|
|
1919
2061
|
try {
|
|
2062
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1920
2063
|
if (args.action === "start") {
|
|
1921
2064
|
if (!args.hostname) {
|
|
1922
2065
|
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
@@ -2014,14 +2157,16 @@ ${res.archiveUrl}`,
|
|
|
2014
2157
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2015
2158
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2016
2159
|
inputSchema: {
|
|
2160
|
+
projectDir: projectDirInput,
|
|
2017
2161
|
category: ticketCategoryEnum,
|
|
2018
|
-
subject:
|
|
2019
|
-
description:
|
|
2020
|
-
contactEmail:
|
|
2162
|
+
subject: z3.string().describe("Short subject line."),
|
|
2163
|
+
description: z3.string().describe("Problem description (no secrets, no card data)."),
|
|
2164
|
+
contactEmail: z3.string().optional().describe("Optional contact email for follow-up.")
|
|
2021
2165
|
}
|
|
2022
2166
|
},
|
|
2023
2167
|
async (args) => {
|
|
2024
2168
|
try {
|
|
2169
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2025
2170
|
const site = requireSiteFile(ctx);
|
|
2026
2171
|
const res = await ctx.client.createTicket(site.credential, {
|
|
2027
2172
|
siteId: site.siteId,
|
|
@@ -2047,18 +2192,20 @@ ${res.archiveUrl}`,
|
|
|
2047
2192
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2048
2193
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
2049
2194
|
inputSchema: {
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2195
|
+
projectDir: projectDirInput,
|
|
2196
|
+
toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
2197
|
+
errorCode: z3.string().optional(),
|
|
2198
|
+
errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
|
|
2199
|
+
requestId: z3.string().optional(),
|
|
2200
|
+
deploymentId: z3.string().optional(),
|
|
2055
2201
|
severity: severityEnum.optional(),
|
|
2056
|
-
description:
|
|
2057
|
-
confirmSubmit:
|
|
2202
|
+
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2203
|
+
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2058
2204
|
}
|
|
2059
2205
|
},
|
|
2060
2206
|
async (args) => {
|
|
2061
2207
|
try {
|
|
2208
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2062
2209
|
const siteState = loadSiteFile(ctx.projectDir);
|
|
2063
2210
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
2064
2211
|
const diagnostics = {
|
|
@@ -2102,39 +2249,40 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
2102
2249
|
}
|
|
2103
2250
|
|
|
2104
2251
|
// src/tools/lifecycle.ts
|
|
2105
|
-
import { z as
|
|
2106
|
-
var deleteConfirmation =
|
|
2107
|
-
siteId:
|
|
2108
|
-
expectedSiteUpdatedAt:
|
|
2109
|
-
expectedStatus:
|
|
2110
|
-
expectedMode:
|
|
2111
|
-
expectedServingMode:
|
|
2112
|
-
expectedShortId:
|
|
2113
|
-
expectedSubscriptionStatus:
|
|
2114
|
-
expectedPlan:
|
|
2115
|
-
expectedCancelAtPeriodEnd:
|
|
2116
|
-
expectedCurrentPeriodEnd:
|
|
2117
|
-
expectedLastDeploymentId:
|
|
2118
|
-
expectedBoundHostnames:
|
|
2119
|
-
acknowledge:
|
|
2252
|
+
import { z as z4 } from "zod";
|
|
2253
|
+
var deleteConfirmation = z4.object({
|
|
2254
|
+
siteId: z4.string().min(1),
|
|
2255
|
+
expectedSiteUpdatedAt: z4.string().datetime(),
|
|
2256
|
+
expectedStatus: z4.enum(["active", "expired", "deleted"]),
|
|
2257
|
+
expectedMode: z4.enum(["free", "paid"]),
|
|
2258
|
+
expectedServingMode: z4.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
2259
|
+
expectedShortId: z4.string().optional(),
|
|
2260
|
+
expectedSubscriptionStatus: z4.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
2261
|
+
expectedPlan: z4.enum(["water", "personal", "share", "business"]).optional(),
|
|
2262
|
+
expectedCancelAtPeriodEnd: z4.boolean().optional(),
|
|
2263
|
+
expectedCurrentPeriodEnd: z4.string().datetime().optional(),
|
|
2264
|
+
expectedLastDeploymentId: z4.string().optional(),
|
|
2265
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
2266
|
+
acknowledge: z4.literal("delete_site_and_cancel_renewal")
|
|
2120
2267
|
});
|
|
2121
|
-
var unbindConfirmation =
|
|
2122
|
-
siteId:
|
|
2123
|
-
bindingId:
|
|
2124
|
-
expectedBindingUpdatedAt:
|
|
2125
|
-
expectedBindingStatus:
|
|
2126
|
-
apexDomain:
|
|
2127
|
-
expectedBoundHostnames:
|
|
2128
|
-
acknowledge:
|
|
2268
|
+
var unbindConfirmation = z4.object({
|
|
2269
|
+
siteId: z4.string().min(1),
|
|
2270
|
+
bindingId: z4.string().min(1),
|
|
2271
|
+
expectedBindingUpdatedAt: z4.string().datetime(),
|
|
2272
|
+
expectedBindingStatus: z4.enum(["provisioning", "active"]),
|
|
2273
|
+
apexDomain: z4.string().min(1),
|
|
2274
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
2275
|
+
acknowledge: z4.literal("unbind_domain_and_remove_custom_hostnames")
|
|
2129
2276
|
});
|
|
2130
|
-
function registerLifecycleTools(server,
|
|
2277
|
+
function registerLifecycleTools(server, baseCtx) {
|
|
2131
2278
|
server.registerTool(
|
|
2132
2279
|
"delete_site",
|
|
2133
2280
|
{
|
|
2134
2281
|
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
2135
2282
|
inputSchema: {
|
|
2136
|
-
|
|
2137
|
-
|
|
2283
|
+
projectDir: projectDirInput,
|
|
2284
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2285
|
+
operationId: z4.string().min(1).optional(),
|
|
2138
2286
|
confirmation: deleteConfirmation.optional()
|
|
2139
2287
|
},
|
|
2140
2288
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2142,6 +2290,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2142
2290
|
},
|
|
2143
2291
|
async (args) => {
|
|
2144
2292
|
try {
|
|
2293
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2145
2294
|
const site = requireSiteFile(ctx);
|
|
2146
2295
|
if (!args.operationId) {
|
|
2147
2296
|
throw new Error("operationId is required for delete_site");
|
|
@@ -2168,13 +2317,14 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2168
2317
|
confirmation: args.confirmation
|
|
2169
2318
|
});
|
|
2170
2319
|
deleteSiteFile(ctx.projectDir);
|
|
2320
|
+
removeCreation(site.siteId);
|
|
2171
2321
|
return structuredToolResult({
|
|
2172
2322
|
schemaVersion: 1,
|
|
2173
2323
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2174
2324
|
resultCode: "site_deleted",
|
|
2175
2325
|
operationId: args.operationId,
|
|
2176
|
-
summary:
|
|
2177
|
-
data: { result },
|
|
2326
|
+
summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
|
|
2327
|
+
data: { result, projectDir: ctx.projectDir },
|
|
2178
2328
|
nextActions: []
|
|
2179
2329
|
});
|
|
2180
2330
|
} catch (error) {
|
|
@@ -2187,8 +2337,9 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2187
2337
|
{
|
|
2188
2338
|
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2189
2339
|
inputSchema: {
|
|
2190
|
-
|
|
2191
|
-
|
|
2340
|
+
projectDir: projectDirInput,
|
|
2341
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2342
|
+
operationId: z4.string().min(1).optional(),
|
|
2192
2343
|
confirmation: unbindConfirmation.optional()
|
|
2193
2344
|
},
|
|
2194
2345
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2196,6 +2347,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2196
2347
|
},
|
|
2197
2348
|
async (args) => {
|
|
2198
2349
|
try {
|
|
2350
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2199
2351
|
const site = requireSiteFile(ctx);
|
|
2200
2352
|
if (!args.operationId) throw new Error("operationId is required for unbind_domain");
|
|
2201
2353
|
if (args.action === "preview") {
|
|
@@ -2226,7 +2378,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2226
2378
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2227
2379
|
resultCode: "domain_unbound",
|
|
2228
2380
|
operationId: args.operationId,
|
|
2229
|
-
summary:
|
|
2381
|
+
summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
|
|
2230
2382
|
data: { result },
|
|
2231
2383
|
nextActions: [{ tool: "site_status", allowed: true }]
|
|
2232
2384
|
});
|
|
@@ -2241,19 +2393,20 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2241
2393
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2242
2394
|
|
|
2243
2395
|
// src/tools/billing.ts
|
|
2244
|
-
import { z as
|
|
2245
|
-
var plan =
|
|
2246
|
-
function registerBillingTools(server,
|
|
2396
|
+
import { z as z5 } from "zod";
|
|
2397
|
+
var plan = z5.enum(["water", "personal", "share", "business"]);
|
|
2398
|
+
function registerBillingTools(server, baseCtx) {
|
|
2247
2399
|
server.registerTool(
|
|
2248
2400
|
"list_billing_plans",
|
|
2249
2401
|
{
|
|
2250
2402
|
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.",
|
|
2251
|
-
inputSchema: {},
|
|
2403
|
+
inputSchema: { projectDir: projectDirInput },
|
|
2252
2404
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2253
2405
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
2254
2406
|
},
|
|
2255
|
-
async () => {
|
|
2407
|
+
async (args) => {
|
|
2256
2408
|
try {
|
|
2409
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2257
2410
|
const catalog = await ctx.client.getBillingPlanCatalog();
|
|
2258
2411
|
return structuredToolResult({
|
|
2259
2412
|
schemaVersion: 1,
|
|
@@ -2273,14 +2426,16 @@ function registerBillingTools(server, ctx) {
|
|
|
2273
2426
|
{
|
|
2274
2427
|
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.",
|
|
2275
2428
|
inputSchema: {
|
|
2429
|
+
projectDir: projectDirInput,
|
|
2276
2430
|
targetPlan: plan,
|
|
2277
|
-
operationId:
|
|
2431
|
+
operationId: z5.string().min(1)
|
|
2278
2432
|
},
|
|
2279
2433
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2280
2434
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2281
2435
|
},
|
|
2282
2436
|
async (args) => {
|
|
2283
2437
|
try {
|
|
2438
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2284
2439
|
const site = requireSiteFile(ctx);
|
|
2285
2440
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
2286
2441
|
siteId: site.siteId,
|
|
@@ -2339,6 +2494,13 @@ Workflow:
|
|
|
2339
2494
|
5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
|
|
2340
2495
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
2341
2496
|
|
|
2497
|
+
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2498
|
+
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2499
|
+
the directory the user is currently working in, on every call. Without it the server falls
|
|
2500
|
+
back to its startup directory, which may be a different project than the one the user is
|
|
2501
|
+
looking at. analyze_site, deploy_site, site_status, refresh_site, delete_site and unbind_domain echo
|
|
2502
|
+
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2503
|
+
|
|
2342
2504
|
Safety boundaries:
|
|
2343
2505
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2344
2506
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|