@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/bin.js
CHANGED
|
@@ -10,6 +10,7 @@ var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
|
10
10
|
var TEST_ACCESS_HEADER = "x-sakupa-test-token";
|
|
11
11
|
var FREE_SITE_TTL_HOURS = 24;
|
|
12
12
|
var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
13
|
+
var FREE_ACTIVE_SITES_PER_IP = 3;
|
|
13
14
|
var PAID_SITE_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
14
15
|
var MAX_FILE_COUNT = 5e3;
|
|
15
16
|
var MAX_SINGLE_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -128,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
128
129
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
129
130
|
|
|
130
131
|
// ../core/dist/domain/version.js
|
|
131
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
132
|
+
var SAKUPA_MCP_VERSION = "0.7.8";
|
|
132
133
|
|
|
133
134
|
// ../core/dist/domain/errors.js
|
|
134
135
|
var HTTP_STATUS = {
|
|
@@ -680,8 +681,8 @@ var HttpApiClient = class {
|
|
|
680
681
|
// src/tools/definitions.ts
|
|
681
682
|
import { randomUUID } from "node:crypto";
|
|
682
683
|
import { promises as fs2 } from "node:fs";
|
|
683
|
-
import { join as
|
|
684
|
-
import { z as
|
|
684
|
+
import { join as join4, resolve as resolve3 } from "node:path";
|
|
685
|
+
import { z as z3 } from "zod";
|
|
685
686
|
|
|
686
687
|
// src/analyze/analyzer.ts
|
|
687
688
|
import { promises as fs } from "node:fs";
|
|
@@ -1203,10 +1204,60 @@ function credentialGitReminder(projectDir) {
|
|
|
1203
1204
|
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).';
|
|
1204
1205
|
}
|
|
1205
1206
|
|
|
1207
|
+
// src/creation-registry.ts
|
|
1208
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1209
|
+
import { homedir } from "node:os";
|
|
1210
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1211
|
+
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
1212
|
+
function creationRegistryPath() {
|
|
1213
|
+
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir();
|
|
1214
|
+
return join3(base, ".sakupa", "created-sites.json");
|
|
1215
|
+
}
|
|
1216
|
+
function readAll() {
|
|
1217
|
+
const path = creationRegistryPath();
|
|
1218
|
+
if (!existsSync2(path)) return [];
|
|
1219
|
+
try {
|
|
1220
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
1221
|
+
if (!Array.isArray(parsed)) return [];
|
|
1222
|
+
return parsed.filter(
|
|
1223
|
+
(e) => typeof e === "object" && e !== null && typeof e.siteId === "string" && typeof e.createdAt === "string"
|
|
1224
|
+
);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return [];
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function writeAll(records) {
|
|
1230
|
+
const path = creationRegistryPath();
|
|
1231
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1232
|
+
writeFileSync2(path, `${JSON.stringify(records, null, 2)}
|
|
1233
|
+
`, "utf-8");
|
|
1234
|
+
}
|
|
1235
|
+
function listRecentCreations(nowMs) {
|
|
1236
|
+
return readAll().filter((e) => {
|
|
1237
|
+
const t = Date.parse(e.createdAt);
|
|
1238
|
+
return Number.isFinite(t) && nowMs - t < RECENT_WINDOW_MS;
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function recordCreation(record) {
|
|
1242
|
+
const rest = readAll().filter((e) => e.siteId !== record.siteId);
|
|
1243
|
+
writeAll([...rest, record]);
|
|
1244
|
+
}
|
|
1245
|
+
function removeCreation(siteId) {
|
|
1246
|
+
const all = readAll();
|
|
1247
|
+
const rest = all.filter((e) => e.siteId !== siteId);
|
|
1248
|
+
if (rest.length !== all.length) writeAll(rest);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1206
1251
|
// src/version.ts
|
|
1207
1252
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
1208
1253
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
1209
1254
|
|
|
1255
|
+
// src/tools/context.ts
|
|
1256
|
+
import { z as z2 } from "zod";
|
|
1257
|
+
import { statSync } from "node:fs";
|
|
1258
|
+
import { homedir as homedir2 } from "node:os";
|
|
1259
|
+
import { isAbsolute, parse, resolve as resolve2 } from "node:path";
|
|
1260
|
+
|
|
1210
1261
|
// src/tools/result.ts
|
|
1211
1262
|
import { z } from "zod";
|
|
1212
1263
|
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
@@ -1249,22 +1300,64 @@ function structuredToolResult(envelope) {
|
|
|
1249
1300
|
}
|
|
1250
1301
|
|
|
1251
1302
|
// src/tools/context.ts
|
|
1303
|
+
var LocalGuidanceError = class extends SakupaError {
|
|
1304
|
+
constructor(code, message) {
|
|
1305
|
+
super(code, message);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
var projectDirInput = z2.string().optional().describe(
|
|
1309
|
+
"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."
|
|
1310
|
+
);
|
|
1311
|
+
function withProjectDir(ctx, projectDirArg) {
|
|
1312
|
+
if (projectDirArg === void 0) return ctx;
|
|
1313
|
+
if (!isAbsolute(projectDirArg)) {
|
|
1314
|
+
throw new LocalGuidanceError(
|
|
1315
|
+
"invalid_request",
|
|
1316
|
+
`projectDir must be an ABSOLUTE path (got "${projectDirArg}"). Pass the full path of the directory the user is currently working in.`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const dir = resolve2(projectDirArg);
|
|
1320
|
+
if (parse(dir).root === dir || dir === homedir2()) {
|
|
1321
|
+
throw new LocalGuidanceError(
|
|
1322
|
+
"invalid_request",
|
|
1323
|
+
`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.`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
const stat = statSync(dir, { throwIfNoEntry: false });
|
|
1327
|
+
if (!stat?.isDirectory()) {
|
|
1328
|
+
throw new LocalGuidanceError(
|
|
1329
|
+
"invalid_request",
|
|
1330
|
+
`projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return { ...ctx, projectDir: dir };
|
|
1334
|
+
}
|
|
1252
1335
|
function requireSiteFile(ctx) {
|
|
1253
1336
|
const state = loadSiteFile(ctx.projectDir);
|
|
1254
1337
|
if (state.kind === "corrupted") {
|
|
1255
|
-
throw new
|
|
1338
|
+
throw new LocalGuidanceError(
|
|
1256
1339
|
"invalid_request",
|
|
1257
1340
|
`.sakupa/site.json in ${ctx.projectDir} is damaged: ${state.problem}. ` + siteFileRecoveryGuidance(ctx.projectDir)
|
|
1258
1341
|
);
|
|
1259
1342
|
}
|
|
1260
1343
|
if (state.kind === "absent") {
|
|
1261
|
-
throw new
|
|
1344
|
+
throw new LocalGuidanceError(
|
|
1262
1345
|
"not_found",
|
|
1263
1346
|
`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.`
|
|
1264
1347
|
);
|
|
1265
1348
|
}
|
|
1266
1349
|
return state.file;
|
|
1267
1350
|
}
|
|
1351
|
+
var STATIC_SUMMARY = {
|
|
1352
|
+
not_found: "The required local project binding or resource is unavailable; if this project has no .sakupa/site.json yet, run deploy_site first.",
|
|
1353
|
+
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.",
|
|
1354
|
+
invalid_request: "The request arguments or local project checks did not pass.",
|
|
1355
|
+
validation_failed: "The request arguments or local project checks did not pass.",
|
|
1356
|
+
state_conflict: "The resource state has changed; re-query the current status before deciding the next step.",
|
|
1357
|
+
confirmation_required: "The site or billing state changed, so the previous confirmation is stale; run the preview again and confirm against the fresh snapshot.",
|
|
1358
|
+
payment_required: "This operation requires an active subscription; check billing_status first.",
|
|
1359
|
+
rate_limited: "The server rate limit was reached; retry after the returned wait time."
|
|
1360
|
+
};
|
|
1268
1361
|
function toolError(e) {
|
|
1269
1362
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1270
1363
|
const retryable = errorCode === "rate_limited" || errorCode === "internal";
|
|
@@ -1283,7 +1376,7 @@ function toolError(e) {
|
|
|
1283
1376
|
)
|
|
1284
1377
|
) : void 0;
|
|
1285
1378
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1286
|
-
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
|
|
1379
|
+
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.");
|
|
1287
1380
|
const result = structuredToolResult({
|
|
1288
1381
|
schemaVersion: 1,
|
|
1289
1382
|
outcome: "failed",
|
|
@@ -1322,12 +1415,12 @@ ${JSON.stringify(obj, null, 2)}`;
|
|
|
1322
1415
|
nextActions: []
|
|
1323
1416
|
});
|
|
1324
1417
|
}
|
|
1325
|
-
var planEnum =
|
|
1326
|
-
var severityEnum =
|
|
1418
|
+
var planEnum = z3.enum(["water", "personal", "share", "business"]);
|
|
1419
|
+
var severityEnum = z3.enum(["low", "medium", "high", "critical"]);
|
|
1327
1420
|
function planCatalog() {
|
|
1328
1421
|
return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
|
|
1329
1422
|
}
|
|
1330
|
-
var ticketCategoryEnum =
|
|
1423
|
+
var ticketCategoryEnum = z3.enum([
|
|
1331
1424
|
"billing",
|
|
1332
1425
|
"payment",
|
|
1333
1426
|
"refund_review",
|
|
@@ -1375,7 +1468,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1375
1468
|
async function buildHashedManifest(files, outputAbs) {
|
|
1376
1469
|
const manifest = [];
|
|
1377
1470
|
for (const file of files) {
|
|
1378
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1471
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, file.path)));
|
|
1379
1472
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1380
1473
|
}
|
|
1381
1474
|
return manifest;
|
|
@@ -1394,7 +1487,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1394
1487
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
1395
1488
|
);
|
|
1396
1489
|
}
|
|
1397
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
1490
|
+
const bytes = new Uint8Array(await fs2.readFile(join4(outputAbs, match.path)));
|
|
1398
1491
|
if (bytes.byteLength !== match.size) {
|
|
1399
1492
|
throw new SakupaError(
|
|
1400
1493
|
"validation_failed",
|
|
@@ -1405,8 +1498,23 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1405
1498
|
}
|
|
1406
1499
|
return targets.length;
|
|
1407
1500
|
}
|
|
1408
|
-
function
|
|
1409
|
-
const
|
|
1501
|
+
function freeSiteCreationBarrier() {
|
|
1502
|
+
const recent = listRecentCreations(Date.now());
|
|
1503
|
+
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
1504
|
+
const registryPath = creationRegistryPath();
|
|
1505
|
+
return text(
|
|
1506
|
+
"local_site_limit_reached",
|
|
1507
|
+
`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.
|
|
1508
|
+
|
|
1509
|
+
` + recent.map((r) => `- ${r.url} (project: ${r.projectDir}, created: ${r.createdAt})`).join("\n") + `
|
|
1510
|
+
|
|
1511
|
+
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.`,
|
|
1512
|
+
{ recentCreations: recent, limit: FREE_ACTIVE_SITES_PER_IP, registryPath },
|
|
1513
|
+
"blocked"
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
function registerTools(server, baseCtx) {
|
|
1517
|
+
const previewHostPattern = previewHostPatternFor(baseCtx.apiBaseUrl);
|
|
1410
1518
|
server.registerTool(
|
|
1411
1519
|
"analyze_site",
|
|
1412
1520
|
{
|
|
@@ -1414,11 +1522,13 @@ function registerTools(server, ctx) {
|
|
|
1414
1522
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1415
1523
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1416
1524
|
inputSchema: {
|
|
1417
|
-
|
|
1525
|
+
projectDir: projectDirInput,
|
|
1526
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
1418
1527
|
}
|
|
1419
1528
|
},
|
|
1420
1529
|
async (args) => {
|
|
1421
1530
|
try {
|
|
1531
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1422
1532
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1423
1533
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1424
1534
|
});
|
|
@@ -1440,18 +1550,20 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1440
1550
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1441
1551
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1442
1552
|
inputSchema: {
|
|
1443
|
-
|
|
1444
|
-
|
|
1553
|
+
projectDir: projectDirInput,
|
|
1554
|
+
outputDir: z3.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1555
|
+
spaFallback: z3.boolean().optional().describe(
|
|
1445
1556
|
"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."
|
|
1446
1557
|
),
|
|
1447
|
-
publicConfirmed:
|
|
1558
|
+
publicConfirmed: z3.boolean().optional().describe(
|
|
1448
1559
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1449
1560
|
),
|
|
1450
|
-
lang:
|
|
1561
|
+
lang: z3.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1451
1562
|
}
|
|
1452
1563
|
},
|
|
1453
1564
|
async (args) => {
|
|
1454
1565
|
try {
|
|
1566
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1455
1567
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
1456
1568
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
1457
1569
|
});
|
|
@@ -1459,7 +1571,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1459
1571
|
return notDeployableResult(analysis);
|
|
1460
1572
|
}
|
|
1461
1573
|
const files = analysis.files;
|
|
1462
|
-
const outputAbs =
|
|
1574
|
+
const outputAbs = resolve3(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1463
1575
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1464
1576
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1465
1577
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -1473,6 +1585,10 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1473
1585
|
);
|
|
1474
1586
|
}
|
|
1475
1587
|
const existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
1588
|
+
if (!existing) {
|
|
1589
|
+
const barrier = freeSiteCreationBarrier();
|
|
1590
|
+
if (barrier) return barrier;
|
|
1591
|
+
}
|
|
1476
1592
|
if (!existing && args.publicConfirmed !== true) {
|
|
1477
1593
|
return text(
|
|
1478
1594
|
"public_deployment_confirmation_required",
|
|
@@ -1494,17 +1610,25 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1494
1610
|
created.siteId,
|
|
1495
1611
|
created.credential
|
|
1496
1612
|
);
|
|
1613
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1497
1614
|
writeSiteFile(ctx.projectDir, {
|
|
1498
1615
|
siteId: created.siteId,
|
|
1499
1616
|
shortId: created.shortId,
|
|
1500
1617
|
url: finalized2.url,
|
|
1501
1618
|
credential: created.credential,
|
|
1502
|
-
createdAt
|
|
1619
|
+
createdAt,
|
|
1503
1620
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1504
1621
|
});
|
|
1622
|
+
recordCreation({
|
|
1623
|
+
siteId: created.siteId,
|
|
1624
|
+
projectDir: ctx.projectDir,
|
|
1625
|
+
url: finalized2.url,
|
|
1626
|
+
createdAt
|
|
1627
|
+
});
|
|
1505
1628
|
return text(
|
|
1506
1629
|
"site_published",
|
|
1507
1630
|
`Site published: ${finalized2.url}
|
|
1631
|
+
Project directory: ${ctx.projectDir}
|
|
1508
1632
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1509
1633
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1510
1634
|
` : "") + `
|
|
@@ -1522,7 +1646,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1522
1646
|
filesUploaded: uploaded2,
|
|
1523
1647
|
totalBytes: finalized2.totalBytes,
|
|
1524
1648
|
warnings: finalized2.warnings,
|
|
1525
|
-
credentialStoredLocally: true
|
|
1649
|
+
credentialStoredLocally: true,
|
|
1650
|
+
projectDir: ctx.projectDir
|
|
1526
1651
|
}
|
|
1527
1652
|
);
|
|
1528
1653
|
}
|
|
@@ -1568,6 +1693,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1568
1693
|
return text(
|
|
1569
1694
|
"site_updated",
|
|
1570
1695
|
`Site updated: ${finalized.url}
|
|
1696
|
+
Project directory: ${ctx.projectDir}
|
|
1571
1697
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1572
1698
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1573
1699
|
` : "") + (finalized.mode === "free" ? `
|
|
@@ -1579,6 +1705,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1579
1705
|
siteId: existing.siteId,
|
|
1580
1706
|
url: finalized.url,
|
|
1581
1707
|
mode: finalized.mode,
|
|
1708
|
+
projectDir: ctx.projectDir,
|
|
1582
1709
|
expiresAt: finalized.expiresAt,
|
|
1583
1710
|
filesUploaded: uploaded,
|
|
1584
1711
|
totalBytes: finalized.totalBytes,
|
|
@@ -1596,17 +1723,18 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1596
1723
|
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.",
|
|
1597
1724
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1598
1725
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1599
|
-
inputSchema: {}
|
|
1726
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1600
1727
|
},
|
|
1601
|
-
async () => {
|
|
1728
|
+
async (args) => {
|
|
1602
1729
|
try {
|
|
1730
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1603
1731
|
const site = requireSiteFile(ctx);
|
|
1604
1732
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
1605
1733
|
return text(
|
|
1606
1734
|
"site_refreshed",
|
|
1607
|
-
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1735
|
+
`Site validity refreshed (project: ${ctx.projectDir}). New expiry: ${res.expiresAt}
|
|
1608
1736
|
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.`,
|
|
1609
|
-
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1737
|
+
{ siteId: site.siteId, expiresAt: res.expiresAt, projectDir: ctx.projectDir }
|
|
1610
1738
|
);
|
|
1611
1739
|
} catch (e) {
|
|
1612
1740
|
return toolError(e);
|
|
@@ -1619,13 +1747,17 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1619
1747
|
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.",
|
|
1620
1748
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1621
1749
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1622
|
-
inputSchema: {}
|
|
1750
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1623
1751
|
},
|
|
1624
|
-
async () => {
|
|
1752
|
+
async (args) => {
|
|
1625
1753
|
try {
|
|
1754
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1626
1755
|
const site = requireSiteFile(ctx);
|
|
1627
1756
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1628
|
-
return textJson("site_status_returned", "Site status:",
|
|
1757
|
+
return textJson("site_status_returned", "Site status:", {
|
|
1758
|
+
...res,
|
|
1759
|
+
projectDir: ctx.projectDir
|
|
1760
|
+
});
|
|
1629
1761
|
} catch (e) {
|
|
1630
1762
|
return toolError(e);
|
|
1631
1763
|
}
|
|
@@ -1638,6 +1770,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1638
1770
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1639
1771
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1640
1772
|
inputSchema: {
|
|
1773
|
+
projectDir: projectDirInput,
|
|
1641
1774
|
plan: planEnum.describe(
|
|
1642
1775
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1643
1776
|
)
|
|
@@ -1645,6 +1778,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1645
1778
|
},
|
|
1646
1779
|
async (args) => {
|
|
1647
1780
|
try {
|
|
1781
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1648
1782
|
const site = requireSiteFile(ctx);
|
|
1649
1783
|
const res = await ctx.client.createPlanCheckout(
|
|
1650
1784
|
{
|
|
@@ -1683,13 +1817,15 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1683
1817
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1684
1818
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1685
1819
|
inputSchema: {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1820
|
+
projectDir: projectDirInput,
|
|
1821
|
+
action: z3.enum(["start", "status"]),
|
|
1822
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
1823
|
+
verificationId: z3.string().optional().describe("Required for status.")
|
|
1689
1824
|
}
|
|
1690
1825
|
},
|
|
1691
1826
|
async (args) => {
|
|
1692
1827
|
try {
|
|
1828
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1693
1829
|
const site = requireSiteFile(ctx);
|
|
1694
1830
|
if (args.action === "status") {
|
|
1695
1831
|
if (!args.verificationId) {
|
|
@@ -1759,10 +1895,11 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1759
1895
|
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).",
|
|
1760
1896
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1761
1897
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1762
|
-
inputSchema: {}
|
|
1898
|
+
inputSchema: { projectDir: projectDirInput }
|
|
1763
1899
|
},
|
|
1764
|
-
async () => {
|
|
1900
|
+
async (args) => {
|
|
1765
1901
|
try {
|
|
1902
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1766
1903
|
const site = requireSiteFile(ctx);
|
|
1767
1904
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
1768
1905
|
const lines = [
|
|
@@ -1792,11 +1929,13 @@ Full status:`, res);
|
|
|
1792
1929
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1793
1930
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1794
1931
|
inputSchema: {
|
|
1795
|
-
|
|
1932
|
+
projectDir: projectDirInput,
|
|
1933
|
+
scope: z3.enum(["site", "public_recovery"])
|
|
1796
1934
|
}
|
|
1797
1935
|
},
|
|
1798
1936
|
async (args) => {
|
|
1799
1937
|
try {
|
|
1938
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1800
1939
|
if (args.scope === "site") {
|
|
1801
1940
|
const site = requireSiteFile(ctx);
|
|
1802
1941
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
@@ -1847,14 +1986,16 @@ Full status:`, res);
|
|
|
1847
1986
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1848
1987
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1849
1988
|
inputSchema: {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1989
|
+
projectDir: projectDirInput,
|
|
1990
|
+
action: z3.enum(["start", "status", "complete"]),
|
|
1991
|
+
hostname: z3.string().optional().describe("Required for start."),
|
|
1992
|
+
verificationId: z3.string().optional().describe("Required for status or complete."),
|
|
1993
|
+
preserveExistingCredentials: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
1854
1994
|
}
|
|
1855
1995
|
},
|
|
1856
1996
|
async (args) => {
|
|
1857
1997
|
try {
|
|
1998
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1858
1999
|
if (args.action === "start") {
|
|
1859
2000
|
if (!args.hostname) {
|
|
1860
2001
|
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
@@ -1952,14 +2093,16 @@ ${res.archiveUrl}`,
|
|
|
1952
2093
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1953
2094
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1954
2095
|
inputSchema: {
|
|
2096
|
+
projectDir: projectDirInput,
|
|
1955
2097
|
category: ticketCategoryEnum,
|
|
1956
|
-
subject:
|
|
1957
|
-
description:
|
|
1958
|
-
contactEmail:
|
|
2098
|
+
subject: z3.string().describe("Short subject line."),
|
|
2099
|
+
description: z3.string().describe("Problem description (no secrets, no card data)."),
|
|
2100
|
+
contactEmail: z3.string().optional().describe("Optional contact email for follow-up.")
|
|
1959
2101
|
}
|
|
1960
2102
|
},
|
|
1961
2103
|
async (args) => {
|
|
1962
2104
|
try {
|
|
2105
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1963
2106
|
const site = requireSiteFile(ctx);
|
|
1964
2107
|
const res = await ctx.client.createTicket(site.credential, {
|
|
1965
2108
|
siteId: site.siteId,
|
|
@@ -1985,18 +2128,20 @@ ${res.archiveUrl}`,
|
|
|
1985
2128
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1986
2129
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1987
2130
|
inputSchema: {
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2131
|
+
projectDir: projectDirInput,
|
|
2132
|
+
toolName: z3.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
2133
|
+
errorCode: z3.string().optional(),
|
|
2134
|
+
errorMessage: z3.string().optional().describe("Sanitized error message (no secrets)."),
|
|
2135
|
+
requestId: z3.string().optional(),
|
|
2136
|
+
deploymentId: z3.string().optional(),
|
|
1993
2137
|
severity: severityEnum.optional(),
|
|
1994
|
-
description:
|
|
1995
|
-
confirmSubmit:
|
|
2138
|
+
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2139
|
+
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
1996
2140
|
}
|
|
1997
2141
|
},
|
|
1998
2142
|
async (args) => {
|
|
1999
2143
|
try {
|
|
2144
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2000
2145
|
const siteState = loadSiteFile(ctx.projectDir);
|
|
2001
2146
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
2002
2147
|
const diagnostics = {
|
|
@@ -2040,19 +2185,20 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
2040
2185
|
}
|
|
2041
2186
|
|
|
2042
2187
|
// src/tools/billing.ts
|
|
2043
|
-
import { z as
|
|
2044
|
-
var plan =
|
|
2045
|
-
function registerBillingTools(server,
|
|
2188
|
+
import { z as z4 } from "zod";
|
|
2189
|
+
var plan = z4.enum(["water", "personal", "share", "business"]);
|
|
2190
|
+
function registerBillingTools(server, baseCtx) {
|
|
2046
2191
|
server.registerTool(
|
|
2047
2192
|
"list_billing_plans",
|
|
2048
2193
|
{
|
|
2049
2194
|
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.",
|
|
2050
|
-
inputSchema: {},
|
|
2195
|
+
inputSchema: { projectDir: projectDirInput },
|
|
2051
2196
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2052
2197
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
2053
2198
|
},
|
|
2054
|
-
async () => {
|
|
2199
|
+
async (args) => {
|
|
2055
2200
|
try {
|
|
2201
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2056
2202
|
const catalog = await ctx.client.getBillingPlanCatalog();
|
|
2057
2203
|
return structuredToolResult({
|
|
2058
2204
|
schemaVersion: 1,
|
|
@@ -2072,14 +2218,16 @@ function registerBillingTools(server, ctx) {
|
|
|
2072
2218
|
{
|
|
2073
2219
|
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.",
|
|
2074
2220
|
inputSchema: {
|
|
2221
|
+
projectDir: projectDirInput,
|
|
2075
2222
|
targetPlan: plan,
|
|
2076
|
-
operationId:
|
|
2223
|
+
operationId: z4.string().min(1)
|
|
2077
2224
|
},
|
|
2078
2225
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2079
2226
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2080
2227
|
},
|
|
2081
2228
|
async (args) => {
|
|
2082
2229
|
try {
|
|
2230
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2083
2231
|
const site = requireSiteFile(ctx);
|
|
2084
2232
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
2085
2233
|
siteId: site.siteId,
|
|
@@ -2109,39 +2257,40 @@ function registerBillingTools(server, ctx) {
|
|
|
2109
2257
|
}
|
|
2110
2258
|
|
|
2111
2259
|
// src/tools/lifecycle.ts
|
|
2112
|
-
import { z as
|
|
2113
|
-
var deleteConfirmation =
|
|
2114
|
-
siteId:
|
|
2115
|
-
expectedSiteUpdatedAt:
|
|
2116
|
-
expectedStatus:
|
|
2117
|
-
expectedMode:
|
|
2118
|
-
expectedServingMode:
|
|
2119
|
-
expectedShortId:
|
|
2120
|
-
expectedSubscriptionStatus:
|
|
2121
|
-
expectedPlan:
|
|
2122
|
-
expectedCancelAtPeriodEnd:
|
|
2123
|
-
expectedCurrentPeriodEnd:
|
|
2124
|
-
expectedLastDeploymentId:
|
|
2125
|
-
expectedBoundHostnames:
|
|
2126
|
-
acknowledge:
|
|
2260
|
+
import { z as z5 } from "zod";
|
|
2261
|
+
var deleteConfirmation = z5.object({
|
|
2262
|
+
siteId: z5.string().min(1),
|
|
2263
|
+
expectedSiteUpdatedAt: z5.string().datetime(),
|
|
2264
|
+
expectedStatus: z5.enum(["active", "expired", "deleted"]),
|
|
2265
|
+
expectedMode: z5.enum(["free", "paid"]),
|
|
2266
|
+
expectedServingMode: z5.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
2267
|
+
expectedShortId: z5.string().optional(),
|
|
2268
|
+
expectedSubscriptionStatus: z5.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
2269
|
+
expectedPlan: z5.enum(["water", "personal", "share", "business"]).optional(),
|
|
2270
|
+
expectedCancelAtPeriodEnd: z5.boolean().optional(),
|
|
2271
|
+
expectedCurrentPeriodEnd: z5.string().datetime().optional(),
|
|
2272
|
+
expectedLastDeploymentId: z5.string().optional(),
|
|
2273
|
+
expectedBoundHostnames: z5.array(z5.string()),
|
|
2274
|
+
acknowledge: z5.literal("delete_site_and_cancel_renewal")
|
|
2127
2275
|
});
|
|
2128
|
-
var unbindConfirmation =
|
|
2129
|
-
siteId:
|
|
2130
|
-
bindingId:
|
|
2131
|
-
expectedBindingUpdatedAt:
|
|
2132
|
-
expectedBindingStatus:
|
|
2133
|
-
apexDomain:
|
|
2134
|
-
expectedBoundHostnames:
|
|
2135
|
-
acknowledge:
|
|
2276
|
+
var unbindConfirmation = z5.object({
|
|
2277
|
+
siteId: z5.string().min(1),
|
|
2278
|
+
bindingId: z5.string().min(1),
|
|
2279
|
+
expectedBindingUpdatedAt: z5.string().datetime(),
|
|
2280
|
+
expectedBindingStatus: z5.enum(["provisioning", "active"]),
|
|
2281
|
+
apexDomain: z5.string().min(1),
|
|
2282
|
+
expectedBoundHostnames: z5.array(z5.string()),
|
|
2283
|
+
acknowledge: z5.literal("unbind_domain_and_remove_custom_hostnames")
|
|
2136
2284
|
});
|
|
2137
|
-
function registerLifecycleTools(server,
|
|
2285
|
+
function registerLifecycleTools(server, baseCtx) {
|
|
2138
2286
|
server.registerTool(
|
|
2139
2287
|
"delete_site",
|
|
2140
2288
|
{
|
|
2141
2289
|
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
2142
2290
|
inputSchema: {
|
|
2143
|
-
|
|
2144
|
-
|
|
2291
|
+
projectDir: projectDirInput,
|
|
2292
|
+
action: z5.enum(["preview", "confirm"]),
|
|
2293
|
+
operationId: z5.string().min(1).optional(),
|
|
2145
2294
|
confirmation: deleteConfirmation.optional()
|
|
2146
2295
|
},
|
|
2147
2296
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2149,6 +2298,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2149
2298
|
},
|
|
2150
2299
|
async (args) => {
|
|
2151
2300
|
try {
|
|
2301
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2152
2302
|
const site = requireSiteFile(ctx);
|
|
2153
2303
|
if (!args.operationId) {
|
|
2154
2304
|
throw new Error("operationId is required for delete_site");
|
|
@@ -2175,13 +2325,14 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2175
2325
|
confirmation: args.confirmation
|
|
2176
2326
|
});
|
|
2177
2327
|
deleteSiteFile(ctx.projectDir);
|
|
2328
|
+
removeCreation(site.siteId);
|
|
2178
2329
|
return structuredToolResult({
|
|
2179
2330
|
schemaVersion: 1,
|
|
2180
2331
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2181
2332
|
resultCode: "site_deleted",
|
|
2182
2333
|
operationId: args.operationId,
|
|
2183
|
-
summary:
|
|
2184
|
-
data: { result },
|
|
2334
|
+
summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
|
|
2335
|
+
data: { result, projectDir: ctx.projectDir },
|
|
2185
2336
|
nextActions: []
|
|
2186
2337
|
});
|
|
2187
2338
|
} catch (error) {
|
|
@@ -2194,8 +2345,9 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2194
2345
|
{
|
|
2195
2346
|
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2196
2347
|
inputSchema: {
|
|
2197
|
-
|
|
2198
|
-
|
|
2348
|
+
projectDir: projectDirInput,
|
|
2349
|
+
action: z5.enum(["preview", "confirm"]),
|
|
2350
|
+
operationId: z5.string().min(1).optional(),
|
|
2199
2351
|
confirmation: unbindConfirmation.optional()
|
|
2200
2352
|
},
|
|
2201
2353
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
@@ -2203,6 +2355,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2203
2355
|
},
|
|
2204
2356
|
async (args) => {
|
|
2205
2357
|
try {
|
|
2358
|
+
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2206
2359
|
const site = requireSiteFile(ctx);
|
|
2207
2360
|
if (!args.operationId) throw new Error("operationId is required for unbind_domain");
|
|
2208
2361
|
if (args.action === "preview") {
|
|
@@ -2233,7 +2386,7 @@ function registerLifecycleTools(server, ctx) {
|
|
|
2233
2386
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2234
2387
|
resultCode: "domain_unbound",
|
|
2235
2388
|
operationId: args.operationId,
|
|
2236
|
-
summary:
|
|
2389
|
+
summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
|
|
2237
2390
|
data: { result },
|
|
2238
2391
|
nextActions: [{ tool: "site_status", allowed: true }]
|
|
2239
2392
|
});
|
|
@@ -2346,6 +2499,13 @@ Workflow:
|
|
|
2346
2499
|
5. create_support_ticket (subscribed sites) opens a support ticket; report_bug sends a
|
|
2347
2500
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
2348
2501
|
|
|
2502
|
+
Project directory contract: ONE directory = ONE site (its .sakupa/site.json holds the
|
|
2503
|
+
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2504
|
+
the directory the user is currently working in, on every call. Without it the server falls
|
|
2505
|
+
back to its startup directory, which may be a different project than the one the user is
|
|
2506
|
+
looking at. analyze_site, deploy_site, site_status, refresh_site, delete_site and unbind_domain echo
|
|
2507
|
+
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2508
|
+
|
|
2349
2509
|
Safety boundaries:
|
|
2350
2510
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
2351
2511
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
@@ -2398,7 +2558,7 @@ async function main() {
|
|
|
2398
2558
|
const transport = new StdioServerTransport();
|
|
2399
2559
|
await server.connect(transport);
|
|
2400
2560
|
console.error(
|
|
2401
|
-
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}, project: ${config.projectDir})`
|
|
2561
|
+
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}, default project: ${config.projectDir}; tools accept per-call projectDir)`
|
|
2402
2562
|
);
|
|
2403
2563
|
}
|
|
2404
2564
|
main().catch((err) => {
|