@sakupa/mcp 0.7.55 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +149 -104
- package/dist/index.js +138 -98
- package/package.json +15 -3
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bin.ts
|
|
4
|
-
import {
|
|
4
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { stdout } from "node:process";
|
|
6
6
|
|
|
7
7
|
// src/project-root.ts
|
|
@@ -402,7 +402,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
// ../core/dist/domain/version.js
|
|
405
|
-
var SAKUPA_MCP_VERSION = "
|
|
405
|
+
var SAKUPA_MCP_VERSION = "1.1.0";
|
|
406
406
|
|
|
407
407
|
// ../core/dist/domain/errors.js
|
|
408
408
|
var HTTP_STATUS = {
|
|
@@ -765,7 +765,11 @@ function environmentFor(apiBaseUrl) {
|
|
|
765
765
|
}
|
|
766
766
|
|
|
767
767
|
// src/server.ts
|
|
768
|
-
import {
|
|
768
|
+
import {
|
|
769
|
+
CLIENT_CAPABILITIES_META_KEY,
|
|
770
|
+
McpServer,
|
|
771
|
+
inputResponse
|
|
772
|
+
} from "@modelcontextprotocol/server";
|
|
769
773
|
|
|
770
774
|
// src/api-client.ts
|
|
771
775
|
var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -2946,10 +2950,21 @@ async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
|
|
|
2946
2950
|
};
|
|
2947
2951
|
}
|
|
2948
2952
|
|
|
2953
|
+
// src/tools/context.ts
|
|
2954
|
+
import {
|
|
2955
|
+
inputRequired
|
|
2956
|
+
} from "@modelcontextprotocol/server";
|
|
2957
|
+
|
|
2949
2958
|
// src/project-binding.ts
|
|
2950
2959
|
import { fileURLToPath } from "node:url";
|
|
2951
2960
|
import { resolve as resolve4 } from "node:path";
|
|
2952
2961
|
var MCP_ROOTS_TIMEOUT_MS = 5e3;
|
|
2962
|
+
var McpRootsPending = class extends Error {
|
|
2963
|
+
constructor() {
|
|
2964
|
+
super("MCP Roots must be requested from the client before the project can be bound.");
|
|
2965
|
+
this.name = "McpRootsPending";
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2953
2968
|
var ProjectBindingError = class extends Error {
|
|
2954
2969
|
diagnostics;
|
|
2955
2970
|
constructor(diagnostics) {
|
|
@@ -2967,10 +2982,10 @@ var ProjectBindingResolver = class {
|
|
|
2967
2982
|
bound;
|
|
2968
2983
|
boundState;
|
|
2969
2984
|
resolving;
|
|
2970
|
-
async resolve() {
|
|
2985
|
+
async resolve(call) {
|
|
2971
2986
|
if (this.bound) return this.bound;
|
|
2972
2987
|
if (this.resolving) return this.resolving;
|
|
2973
|
-
this.resolving = this.inspect().then((inspection) => {
|
|
2988
|
+
this.resolving = this.inspect(false, call).then((inspection) => {
|
|
2974
2989
|
if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
|
|
2975
2990
|
this.bound = inspection.selected;
|
|
2976
2991
|
this.boundState = inspection.diagnostics;
|
|
@@ -2980,18 +2995,18 @@ var ProjectBindingResolver = class {
|
|
|
2980
2995
|
});
|
|
2981
2996
|
return this.resolving;
|
|
2982
2997
|
}
|
|
2983
|
-
async diagnose() {
|
|
2998
|
+
async diagnose(call) {
|
|
2984
2999
|
if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
|
|
2985
|
-
const inspection = await this.inspect();
|
|
3000
|
+
const inspection = await this.inspect(false, call);
|
|
2986
3001
|
if (inspection.selected) {
|
|
2987
3002
|
this.bound = inspection.selected;
|
|
2988
3003
|
this.boundState = inspection.diagnostics;
|
|
2989
3004
|
}
|
|
2990
3005
|
return inspection.diagnostics;
|
|
2991
3006
|
}
|
|
2992
|
-
async initialize() {
|
|
3007
|
+
async initialize(call) {
|
|
2993
3008
|
if (this.bound) return this.bound;
|
|
2994
|
-
const inspection = await this.inspect(true);
|
|
3009
|
+
const inspection = await this.inspect(true, call);
|
|
2995
3010
|
if (inspection.selected) {
|
|
2996
3011
|
this.bound = inspection.selected;
|
|
2997
3012
|
this.boundState = inspection.diagnostics;
|
|
@@ -3010,8 +3025,8 @@ var ProjectBindingResolver = class {
|
|
|
3010
3025
|
);
|
|
3011
3026
|
return this.bound;
|
|
3012
3027
|
}
|
|
3013
|
-
async inspect(forInitialization = false) {
|
|
3014
|
-
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs);
|
|
3028
|
+
async inspect(forInitialization = false, call) {
|
|
3029
|
+
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
|
|
3015
3030
|
const rootCandidates = snapshot.roots.map(inspectRoot);
|
|
3016
3031
|
const initializedRoots = rootCandidates.filter(
|
|
3017
3032
|
(candidate) => candidate.initialized && candidate.path !== void 0
|
|
@@ -3139,11 +3154,16 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
|
|
|
3139
3154
|
if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
|
|
3140
3155
|
return fileURLToPath(parsed, { windows });
|
|
3141
3156
|
}
|
|
3142
|
-
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS) {
|
|
3157
|
+
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
|
|
3143
3158
|
if (!provider) return { supported: false, roots: [] };
|
|
3144
3159
|
try {
|
|
3145
|
-
return await withOperationTimeout(
|
|
3160
|
+
return await withOperationTimeout(
|
|
3161
|
+
"MCP Roots request",
|
|
3162
|
+
timeoutMs,
|
|
3163
|
+
() => provider(call)
|
|
3164
|
+
);
|
|
3146
3165
|
} catch (error) {
|
|
3166
|
+
if (error instanceof McpRootsPending) throw error;
|
|
3147
3167
|
return {
|
|
3148
3168
|
supported: true,
|
|
3149
3169
|
roots: [],
|
|
@@ -3221,7 +3241,7 @@ var TARGET_MCP_TOOL_NAMES = [
|
|
|
3221
3241
|
"support",
|
|
3222
3242
|
"report"
|
|
3223
3243
|
];
|
|
3224
|
-
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
3244
|
+
var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
|
|
3225
3245
|
schemaVersion: z.literal(1),
|
|
3226
3246
|
outcome: z.enum([
|
|
3227
3247
|
"completed",
|
|
@@ -3303,7 +3323,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
|
3303
3323
|
reasonCode: z.string().optional()
|
|
3304
3324
|
})
|
|
3305
3325
|
)
|
|
3306
|
-
};
|
|
3326
|
+
});
|
|
3307
3327
|
function structuredToolResult(envelope) {
|
|
3308
3328
|
const clientTimeZone = clientRuntimeTimeZone();
|
|
3309
3329
|
const presentation = {
|
|
@@ -3417,9 +3437,9 @@ function resolverFor(ctx) {
|
|
|
3417
3437
|
}
|
|
3418
3438
|
return resolver;
|
|
3419
3439
|
}
|
|
3420
|
-
async function withProjectDir(ctx) {
|
|
3440
|
+
async function withProjectDir(ctx, call) {
|
|
3421
3441
|
try {
|
|
3422
|
-
const binding = await resolverFor(ctx).resolve();
|
|
3442
|
+
const binding = await resolverFor(ctx).resolve(call);
|
|
3423
3443
|
const resolved = resolveLockedProjectRoot(binding.projectDir);
|
|
3424
3444
|
return {
|
|
3425
3445
|
...ctx,
|
|
@@ -3442,12 +3462,12 @@ async function withProjectDir(ctx) {
|
|
|
3442
3462
|
throw error;
|
|
3443
3463
|
}
|
|
3444
3464
|
}
|
|
3445
|
-
async function diagnoseProjectBinding(ctx) {
|
|
3446
|
-
return resolverFor(ctx).diagnose();
|
|
3465
|
+
async function diagnoseProjectBinding(ctx, call) {
|
|
3466
|
+
return resolverFor(ctx).diagnose(call);
|
|
3447
3467
|
}
|
|
3448
|
-
async function initializeWorkspaceProject(ctx) {
|
|
3468
|
+
async function initializeWorkspaceProject(ctx, call) {
|
|
3449
3469
|
try {
|
|
3450
|
-
const resolved = await resolverFor(ctx).initialize();
|
|
3470
|
+
const resolved = await resolverFor(ctx).initialize(call);
|
|
3451
3471
|
return {
|
|
3452
3472
|
...ctx,
|
|
3453
3473
|
projectDir: resolved.projectDir,
|
|
@@ -3463,10 +3483,11 @@ async function initializeWorkspaceProject(ctx) {
|
|
|
3463
3483
|
throw error;
|
|
3464
3484
|
}
|
|
3465
3485
|
}
|
|
3466
|
-
async function optionalProjectContext(ctx) {
|
|
3486
|
+
async function optionalProjectContext(ctx, call) {
|
|
3467
3487
|
try {
|
|
3468
|
-
return await withProjectDir(ctx);
|
|
3469
|
-
} catch {
|
|
3488
|
+
return await withProjectDir(ctx, call);
|
|
3489
|
+
} catch (error) {
|
|
3490
|
+
if (error instanceof McpRootsPending) throw error;
|
|
3470
3491
|
return null;
|
|
3471
3492
|
}
|
|
3472
3493
|
}
|
|
@@ -3514,6 +3535,9 @@ function requireSiteFile(ctx) {
|
|
|
3514
3535
|
}
|
|
3515
3536
|
var UNAUTHORIZED_SUMMARY = "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.";
|
|
3516
3537
|
function toolError(e) {
|
|
3538
|
+
if (e instanceof McpRootsPending) {
|
|
3539
|
+
return inputRequired({ inputRequests: { roots: inputRequired.listRoots() } });
|
|
3540
|
+
}
|
|
3517
3541
|
const isSakupa = isSakupaError(e);
|
|
3518
3542
|
const errorCode = isSakupa ? e.code : "internal";
|
|
3519
3543
|
const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
|
|
@@ -4030,13 +4054,13 @@ function registerTools(server, baseCtx) {
|
|
|
4030
4054
|
description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy.",
|
|
4031
4055
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4032
4056
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
4033
|
-
inputSchema: {
|
|
4057
|
+
inputSchema: z2.object({
|
|
4034
4058
|
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
4035
|
-
}
|
|
4059
|
+
})
|
|
4036
4060
|
},
|
|
4037
|
-
async (args) => {
|
|
4061
|
+
async (args, call) => {
|
|
4038
4062
|
try {
|
|
4039
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4063
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4040
4064
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
4041
4065
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
4042
4066
|
});
|
|
@@ -4057,7 +4081,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4057
4081
|
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
|
|
4058
4082
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4059
4083
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4060
|
-
inputSchema: {
|
|
4084
|
+
inputSchema: z2.object({
|
|
4061
4085
|
outputDir: z2.string().min(1).describe(
|
|
4062
4086
|
'REQUIRED: exact publish directory relative to the initialized project root, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). Sakupa applies it only inside the cwd-locked project.'
|
|
4063
4087
|
),
|
|
@@ -4083,12 +4107,12 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4083
4107
|
"Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
|
|
4084
4108
|
),
|
|
4085
4109
|
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
4086
|
-
}
|
|
4110
|
+
})
|
|
4087
4111
|
},
|
|
4088
|
-
async (args) => {
|
|
4112
|
+
async (args, call) => {
|
|
4089
4113
|
let releaseHandoffLock;
|
|
4090
4114
|
try {
|
|
4091
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4115
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4092
4116
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
4093
4117
|
if (!analysis.deployable || !analysis.files) {
|
|
4094
4118
|
return notDeployableResult(analysis);
|
|
@@ -4713,11 +4737,11 @@ Optional security recommendation: this management credential was created at ${ti
|
|
|
4713
4737
|
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscription-backed sites have no free-site expiry while the subscription remains active and need no refresh.",
|
|
4714
4738
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4715
4739
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4716
|
-
inputSchema: {}
|
|
4740
|
+
inputSchema: z2.object({})
|
|
4717
4741
|
},
|
|
4718
|
-
async () => {
|
|
4742
|
+
async (_args, call) => {
|
|
4719
4743
|
try {
|
|
4720
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4744
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4721
4745
|
const site = requireSiteFile(ctx);
|
|
4722
4746
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
4723
4747
|
if (site.url) {
|
|
@@ -4746,11 +4770,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4746
4770
|
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, custom domains, size, last deployment and warnings. For a paid site this tool also automatically returns the complete authoritative billing snapshot; users never need to know or name a separate billing tool to get accurate subscription information.",
|
|
4747
4771
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4748
4772
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
4749
|
-
inputSchema: {}
|
|
4773
|
+
inputSchema: z2.object({})
|
|
4750
4774
|
},
|
|
4751
|
-
async () => {
|
|
4775
|
+
async (_args, call) => {
|
|
4752
4776
|
try {
|
|
4753
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4777
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4754
4778
|
const site = requireSiteFile(ctx);
|
|
4755
4779
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
4756
4780
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -4777,15 +4801,15 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4777
4801
|
description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 24-hour expiry. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
|
|
4778
4802
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4779
4803
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4780
|
-
inputSchema: {
|
|
4804
|
+
inputSchema: z2.object({
|
|
4781
4805
|
plan: planEnum.describe(
|
|
4782
4806
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
4783
4807
|
)
|
|
4784
|
-
}
|
|
4808
|
+
})
|
|
4785
4809
|
},
|
|
4786
|
-
async (args) => {
|
|
4810
|
+
async (args, call) => {
|
|
4787
4811
|
try {
|
|
4788
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4812
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4789
4813
|
const site = requireSiteFile(ctx);
|
|
4790
4814
|
const res = await ctx.client.createPlanCheckout(
|
|
4791
4815
|
{
|
|
@@ -4823,17 +4847,17 @@ Once Stripe confirms payment and Sakupa synchronizes the subscription, the curre
|
|
|
4823
4847
|
description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the subscription-backed ${previewHostPattern} URL keeps working alongside it while the subscription is active. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. One apex TXT verification covers both. A site has one FINAL apex domain; starting a different apex begins a zero-downtime switch and the previous domain remains until the new www is live. The www CNAME must remain while bound. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and subscription-backed Sakupa URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
|
|
4824
4848
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4825
4849
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4826
|
-
inputSchema: {
|
|
4850
|
+
inputSchema: z2.object({
|
|
4827
4851
|
action: z2.enum(["start", "status"]),
|
|
4828
4852
|
hostname: z2.string().optional().describe("Required for start."),
|
|
4829
4853
|
verificationId: z2.string().optional().describe(
|
|
4830
4854
|
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
4831
4855
|
)
|
|
4832
|
-
}
|
|
4856
|
+
})
|
|
4833
4857
|
},
|
|
4834
|
-
async (args) => {
|
|
4858
|
+
async (args, call) => {
|
|
4835
4859
|
try {
|
|
4836
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4860
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4837
4861
|
const site = requireSiteFile(ctx);
|
|
4838
4862
|
if (args.action === "status") {
|
|
4839
4863
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -4962,11 +4986,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
4962
4986
|
description: "Return the sole authoritative source for this site's hosting subscription: current plan, next renewal plan or cancellation, effective time, payment state, current paid entitlement, reconciled paid usage or current free-site fair-use telemetry, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
|
|
4963
4987
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4964
4988
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
4965
|
-
inputSchema: {}
|
|
4989
|
+
inputSchema: z2.object({})
|
|
4966
4990
|
},
|
|
4967
|
-
async () => {
|
|
4991
|
+
async (_args, call) => {
|
|
4968
4992
|
try {
|
|
4969
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4993
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4970
4994
|
const site = requireSiteFile(ctx);
|
|
4971
4995
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
4972
4996
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -5006,14 +5030,14 @@ Full status:`, res);
|
|
|
5006
5030
|
description: "Open the Stripe-hosted billing portal for this site: update the payment method, view invoices, or cancel the subscription. All billing operations happen on the Stripe-hosted page \u2014 never inside the AI tool. With .sakupa/site.json, this opens the site-specific portal. Without the local credential, this returns Stripe's public no-code Customer Portal login page. The customer enters the checkout email and confirms a one-time passcode sent by Stripe. This never restores Sakupa site authority.",
|
|
5007
5031
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5008
5032
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5009
|
-
inputSchema: {
|
|
5033
|
+
inputSchema: z2.object({
|
|
5010
5034
|
scope: z2.enum(["site", "public_recovery"])
|
|
5011
|
-
}
|
|
5035
|
+
})
|
|
5012
5036
|
},
|
|
5013
|
-
async (args) => {
|
|
5037
|
+
async (args, call) => {
|
|
5014
5038
|
try {
|
|
5015
5039
|
if (args.scope === "site") {
|
|
5016
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5040
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5017
5041
|
const site = requireSiteFile(ctx);
|
|
5018
5042
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
5019
5043
|
return structuredToolResult({
|
|
@@ -5062,7 +5086,7 @@ Full status:`, res);
|
|
|
5062
5086
|
description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Recovery is resumable: start stores local pending state; complete installs and writes the new .sakupa/site.json credential BEFORE requesting content; download uses that credential to reissue an archive and safely extract it into the explicitly selected outputDir without repeating DNS.",
|
|
5063
5087
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5064
5088
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
5065
|
-
inputSchema: {
|
|
5089
|
+
inputSchema: z2.object({
|
|
5066
5090
|
action: z2.enum(["start", "status", "complete", "download"]),
|
|
5067
5091
|
hostname: z2.string().optional().describe("Required for start."),
|
|
5068
5092
|
verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
@@ -5070,11 +5094,11 @@ Full status:`, res);
|
|
|
5070
5094
|
"REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
|
|
5071
5095
|
),
|
|
5072
5096
|
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
5073
|
-
}
|
|
5097
|
+
})
|
|
5074
5098
|
},
|
|
5075
|
-
async (args) => {
|
|
5099
|
+
async (args, call) => {
|
|
5076
5100
|
try {
|
|
5077
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5101
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5078
5102
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
5079
5103
|
throw new LocalGuidanceError(
|
|
5080
5104
|
"invalid_request",
|
|
@@ -5423,16 +5447,16 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5423
5447
|
description: "Create a Sakupa support ticket for billing, payment, refund review, domain verification, deployment, serving or other issues the MCP cannot solve automatically. Do not include secrets, credentials or card data in the description.",
|
|
5424
5448
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5425
5449
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5426
|
-
inputSchema: {
|
|
5450
|
+
inputSchema: z2.object({
|
|
5427
5451
|
category: ticketCategoryEnum,
|
|
5428
5452
|
subject: z2.string().describe("Short subject line."),
|
|
5429
5453
|
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
5430
5454
|
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
5431
|
-
}
|
|
5455
|
+
})
|
|
5432
5456
|
},
|
|
5433
|
-
async (args) => {
|
|
5457
|
+
async (args, call) => {
|
|
5434
5458
|
try {
|
|
5435
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5459
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5436
5460
|
const site = requireSiteFile(ctx);
|
|
5437
5461
|
const res = await ctx.client.createTicket(site.credential, {
|
|
5438
5462
|
siteId: site.siteId,
|
|
@@ -5457,7 +5481,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5457
5481
|
description: "LAST RESORT after help explicitly returns reportRecommended:true. Prepare and submit a sanitized product bug report using helpAuthorization from that diagnosis. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
|
|
5458
5482
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5459
5483
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5460
|
-
inputSchema: {
|
|
5484
|
+
inputSchema: z2.object({
|
|
5461
5485
|
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
5462
5486
|
helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
|
|
5463
5487
|
errorCode: z2.string().optional(),
|
|
@@ -5473,12 +5497,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5473
5497
|
"OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
|
|
5474
5498
|
),
|
|
5475
5499
|
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
5476
|
-
}
|
|
5500
|
+
})
|
|
5477
5501
|
},
|
|
5478
|
-
async (args) => {
|
|
5502
|
+
async (args, call) => {
|
|
5479
5503
|
try {
|
|
5480
5504
|
requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
|
|
5481
|
-
const ctx = await optionalProjectContext(baseCtx);
|
|
5505
|
+
const ctx = await optionalProjectContext(baseCtx, call);
|
|
5482
5506
|
const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
|
|
5483
5507
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
5484
5508
|
const diagnostics = {
|
|
@@ -5558,11 +5582,11 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5558
5582
|
"plans",
|
|
5559
5583
|
{
|
|
5560
5584
|
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.",
|
|
5561
|
-
inputSchema: {},
|
|
5585
|
+
inputSchema: z3.object({}),
|
|
5562
5586
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5563
5587
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
5564
5588
|
},
|
|
5565
|
-
async () => {
|
|
5589
|
+
async (_args, call) => {
|
|
5566
5590
|
try {
|
|
5567
5591
|
const catalog = await baseCtx.client.getBillingPlanCatalog();
|
|
5568
5592
|
return structuredToolResult({
|
|
@@ -5582,15 +5606,15 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5582
5606
|
"change",
|
|
5583
5607
|
{
|
|
5584
5608
|
description: "Create one Stripe-hosted subscription-management link. The user chooses the plan or period-end cancellation on Stripe; Sakupa never infers intent from the conversation. Creating the link does not change billing.",
|
|
5585
|
-
inputSchema: {
|
|
5609
|
+
inputSchema: z3.object({
|
|
5586
5610
|
operationId: z3.string().min(1)
|
|
5587
|
-
},
|
|
5611
|
+
}),
|
|
5588
5612
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5589
5613
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
5590
5614
|
},
|
|
5591
|
-
async (args) => {
|
|
5615
|
+
async (args, call) => {
|
|
5592
5616
|
try {
|
|
5593
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5617
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5594
5618
|
const site = requireSiteFile(ctx);
|
|
5595
5619
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
5596
5620
|
siteId: site.siteId,
|
|
@@ -5873,13 +5897,13 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5873
5897
|
"init",
|
|
5874
5898
|
{
|
|
5875
5899
|
description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. If MCP Roots are unavailable, call help; the AI may then use the no-argument CLI init itself.",
|
|
5876
|
-
inputSchema: {},
|
|
5900
|
+
inputSchema: z4.object({}),
|
|
5877
5901
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5878
5902
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
5879
5903
|
},
|
|
5880
|
-
async () => {
|
|
5904
|
+
async (_args, call) => {
|
|
5881
5905
|
try {
|
|
5882
|
-
const ctx = await initializeWorkspaceProject(baseCtx);
|
|
5906
|
+
const ctx = await initializeWorkspaceProject(baseCtx, call);
|
|
5883
5907
|
const marker = loadProjectMarker(ctx.projectDir);
|
|
5884
5908
|
if (marker.kind !== "ok")
|
|
5885
5909
|
throw new Error("init postcondition failed: project marker missing");
|
|
@@ -5913,17 +5937,17 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5913
5937
|
"help",
|
|
5914
5938
|
{
|
|
5915
5939
|
description: "FIRST troubleshooting tool for every Sakupa difficulty. With topic diagnose (default), inspect MCP Roots, cwd, binding and local state without requiring a project or calling the API. Use overview, terminology, or a tool name for complete usage, side effects, parameters and warnings. Only recommend report when help explicitly returns reportRecommended:true.",
|
|
5916
|
-
inputSchema: {
|
|
5940
|
+
inputSchema: z4.object({
|
|
5917
5941
|
topic: z4.enum(HELP_TOPICS).optional().default("diagnose"),
|
|
5918
5942
|
failedTool: z4.string().optional(),
|
|
5919
5943
|
errorCode: z4.string().optional(),
|
|
5920
5944
|
resultCode: z4.string().optional(),
|
|
5921
5945
|
requestId: z4.string().optional()
|
|
5922
|
-
},
|
|
5946
|
+
}),
|
|
5923
5947
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5924
5948
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
5925
5949
|
},
|
|
5926
|
-
async (args) => {
|
|
5950
|
+
async (args, call) => {
|
|
5927
5951
|
try {
|
|
5928
5952
|
if (args.topic === "overview") {
|
|
5929
5953
|
const catalog = Object.fromEntries(
|
|
@@ -5985,7 +6009,7 @@ Terminology: ${terminologyText}` : ""),
|
|
|
5985
6009
|
nextActions: []
|
|
5986
6010
|
});
|
|
5987
6011
|
}
|
|
5988
|
-
const diagnosis = await diagnoseProjectBinding(baseCtx);
|
|
6012
|
+
const diagnosis = await diagnoseProjectBinding(baseCtx, call);
|
|
5989
6013
|
const selected = diagnosis.selectedProjectDir;
|
|
5990
6014
|
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
5991
6015
|
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
@@ -6064,18 +6088,18 @@ function registerCredentialTools(server, baseCtx) {
|
|
|
6064
6088
|
"rotate",
|
|
6065
6089
|
{
|
|
6066
6090
|
description: "Optionally rotate this site management credential. The first call is a read-only preview. Only confirmed:true after explicit user approval installs a locally generated new credential and revokes every previous credential. Rotation is never required to deploy.",
|
|
6067
|
-
inputSchema: {
|
|
6091
|
+
inputSchema: z5.object({
|
|
6068
6092
|
confirmed: z5.boolean().optional().describe(
|
|
6069
6093
|
"True only after showing the rotate preview and the user explicitly approves revoking every old credential."
|
|
6070
6094
|
)
|
|
6071
|
-
},
|
|
6095
|
+
}),
|
|
6072
6096
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
6073
6097
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
6074
6098
|
},
|
|
6075
|
-
async (args) => {
|
|
6099
|
+
async (args, call) => {
|
|
6076
6100
|
let releaseLock;
|
|
6077
6101
|
try {
|
|
6078
|
-
const ctx = await withProjectDir(baseCtx);
|
|
6102
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
6079
6103
|
let site = requireSiteFile(ctx);
|
|
6080
6104
|
const pending = loadCredentialRotation(ctx.projectDir);
|
|
6081
6105
|
if (pending.kind !== "absent" || args.confirmed === true) {
|
|
@@ -6466,23 +6490,7 @@ function createSakupaMcpServer(opts) {
|
|
|
6466
6490
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
6467
6491
|
);
|
|
6468
6492
|
const processCwd = resolve6(opts.projectDir ?? process.cwd());
|
|
6469
|
-
const rootsProvider = opts.rootsProvider ?? (
|
|
6470
|
-
const capabilities = server.server.getClientCapabilities();
|
|
6471
|
-
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
6472
|
-
try {
|
|
6473
|
-
const response = await server.server.listRoots(void 0, {
|
|
6474
|
-
timeout: MCP_ROOTS_TIMEOUT_MS,
|
|
6475
|
-
maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
|
|
6476
|
-
});
|
|
6477
|
-
return { supported: true, roots: response.roots };
|
|
6478
|
-
} catch (error) {
|
|
6479
|
-
return {
|
|
6480
|
-
supported: true,
|
|
6481
|
-
roots: [],
|
|
6482
|
-
error: error instanceof Error ? error.message : String(error)
|
|
6483
|
-
};
|
|
6484
|
-
}
|
|
6485
|
-
});
|
|
6493
|
+
const rootsProvider = opts.rootsProvider ?? ((call) => readClientRoots(server, call));
|
|
6486
6494
|
const ctx = {
|
|
6487
6495
|
client,
|
|
6488
6496
|
apiBaseUrl: opts.apiBaseUrl,
|
|
@@ -6496,6 +6504,38 @@ function createSakupaMcpServer(opts) {
|
|
|
6496
6504
|
registerHelpTools(server, ctx);
|
|
6497
6505
|
return server;
|
|
6498
6506
|
}
|
|
6507
|
+
async function readClientRoots(server, call) {
|
|
6508
|
+
if (call?.mcpReq.envelope !== void 0) {
|
|
6509
|
+
const envelope = call.mcpReq.envelope;
|
|
6510
|
+
const declared = envelope[CLIENT_CAPABILITIES_META_KEY];
|
|
6511
|
+
if (!declared?.roots) return { supported: false, roots: [] };
|
|
6512
|
+
const answered = inputResponse(call.mcpReq.inputResponses, "roots");
|
|
6513
|
+
if (answered.kind === "roots") return { supported: true, roots: answered.roots };
|
|
6514
|
+
if (call.mcpReq.inputResponses !== void 0) {
|
|
6515
|
+
return {
|
|
6516
|
+
supported: true,
|
|
6517
|
+
roots: [],
|
|
6518
|
+
error: "The client retried without answering the embedded roots/list request."
|
|
6519
|
+
};
|
|
6520
|
+
}
|
|
6521
|
+
throw new McpRootsPending();
|
|
6522
|
+
}
|
|
6523
|
+
const capabilities = server.server.getClientCapabilities();
|
|
6524
|
+
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
6525
|
+
try {
|
|
6526
|
+
const response = await server.server.listRoots(void 0, {
|
|
6527
|
+
timeout: MCP_ROOTS_TIMEOUT_MS,
|
|
6528
|
+
maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
|
|
6529
|
+
});
|
|
6530
|
+
return { supported: true, roots: response.roots };
|
|
6531
|
+
} catch (error) {
|
|
6532
|
+
return {
|
|
6533
|
+
supported: true,
|
|
6534
|
+
roots: [],
|
|
6535
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6536
|
+
};
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6499
6539
|
|
|
6500
6540
|
// src/bin.ts
|
|
6501
6541
|
async function main() {
|
|
@@ -6508,15 +6548,20 @@ async function main() {
|
|
|
6508
6548
|
process.exitCode = result.exitCode;
|
|
6509
6549
|
return;
|
|
6510
6550
|
}
|
|
6551
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
6552
|
+
stdout.write(`${MCP_VERSION}
|
|
6553
|
+
`);
|
|
6554
|
+
return;
|
|
6555
|
+
}
|
|
6511
6556
|
if (argv.length > 0) {
|
|
6512
|
-
throw new Error("Usage: sakupa-mcp [init]");
|
|
6557
|
+
throw new Error("Usage: sakupa-mcp [init | --version]");
|
|
6513
6558
|
}
|
|
6514
6559
|
const config = loadMcpRuntimeConfig();
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6560
|
+
serveStdio(() => createSakupaMcpServer(config), {
|
|
6561
|
+
onerror: (error) => console.error("[sakupa-mcp] transport error:", error.message)
|
|
6562
|
+
});
|
|
6518
6563
|
console.error(
|
|
6519
|
-
`[sakupa-mcp] v${MCP_VERSION}
|
|
6564
|
+
`[sakupa-mcp] v${MCP_VERSION} serving stdio (api: ${config.apiBaseUrl}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
|
|
6520
6565
|
);
|
|
6521
6566
|
}
|
|
6522
6567
|
main().catch((err2) => {
|
package/dist/index.js
CHANGED
|
@@ -147,7 +147,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// ../core/dist/domain/version.js
|
|
150
|
-
var SAKUPA_MCP_VERSION = "
|
|
150
|
+
var SAKUPA_MCP_VERSION = "1.1.0";
|
|
151
151
|
|
|
152
152
|
// ../core/dist/domain/errors.js
|
|
153
153
|
var HTTP_STATUS = {
|
|
@@ -1536,6 +1536,11 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1536
1536
|
};
|
|
1537
1537
|
}
|
|
1538
1538
|
|
|
1539
|
+
// src/tools/context.ts
|
|
1540
|
+
import {
|
|
1541
|
+
inputRequired
|
|
1542
|
+
} from "@modelcontextprotocol/server";
|
|
1543
|
+
|
|
1539
1544
|
// src/project-binding.ts
|
|
1540
1545
|
import { fileURLToPath } from "node:url";
|
|
1541
1546
|
import { resolve as resolve3 } from "node:path";
|
|
@@ -1763,6 +1768,12 @@ function writeMarkerAtomically(projectDir, marker) {
|
|
|
1763
1768
|
|
|
1764
1769
|
// src/project-binding.ts
|
|
1765
1770
|
var MCP_ROOTS_TIMEOUT_MS = 5e3;
|
|
1771
|
+
var McpRootsPending = class extends Error {
|
|
1772
|
+
constructor() {
|
|
1773
|
+
super("MCP Roots must be requested from the client before the project can be bound.");
|
|
1774
|
+
this.name = "McpRootsPending";
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1766
1777
|
var ProjectBindingError = class extends Error {
|
|
1767
1778
|
diagnostics;
|
|
1768
1779
|
constructor(diagnostics) {
|
|
@@ -1780,10 +1791,10 @@ var ProjectBindingResolver = class {
|
|
|
1780
1791
|
bound;
|
|
1781
1792
|
boundState;
|
|
1782
1793
|
resolving;
|
|
1783
|
-
async resolve() {
|
|
1794
|
+
async resolve(call) {
|
|
1784
1795
|
if (this.bound) return this.bound;
|
|
1785
1796
|
if (this.resolving) return this.resolving;
|
|
1786
|
-
this.resolving = this.inspect().then((inspection) => {
|
|
1797
|
+
this.resolving = this.inspect(false, call).then((inspection) => {
|
|
1787
1798
|
if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
|
|
1788
1799
|
this.bound = inspection.selected;
|
|
1789
1800
|
this.boundState = inspection.diagnostics;
|
|
@@ -1793,18 +1804,18 @@ var ProjectBindingResolver = class {
|
|
|
1793
1804
|
});
|
|
1794
1805
|
return this.resolving;
|
|
1795
1806
|
}
|
|
1796
|
-
async diagnose() {
|
|
1807
|
+
async diagnose(call) {
|
|
1797
1808
|
if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
|
|
1798
|
-
const inspection = await this.inspect();
|
|
1809
|
+
const inspection = await this.inspect(false, call);
|
|
1799
1810
|
if (inspection.selected) {
|
|
1800
1811
|
this.bound = inspection.selected;
|
|
1801
1812
|
this.boundState = inspection.diagnostics;
|
|
1802
1813
|
}
|
|
1803
1814
|
return inspection.diagnostics;
|
|
1804
1815
|
}
|
|
1805
|
-
async initialize() {
|
|
1816
|
+
async initialize(call) {
|
|
1806
1817
|
if (this.bound) return this.bound;
|
|
1807
|
-
const inspection = await this.inspect(true);
|
|
1818
|
+
const inspection = await this.inspect(true, call);
|
|
1808
1819
|
if (inspection.selected) {
|
|
1809
1820
|
this.bound = inspection.selected;
|
|
1810
1821
|
this.boundState = inspection.diagnostics;
|
|
@@ -1823,8 +1834,8 @@ var ProjectBindingResolver = class {
|
|
|
1823
1834
|
);
|
|
1824
1835
|
return this.bound;
|
|
1825
1836
|
}
|
|
1826
|
-
async inspect(forInitialization = false) {
|
|
1827
|
-
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs);
|
|
1837
|
+
async inspect(forInitialization = false, call) {
|
|
1838
|
+
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
|
|
1828
1839
|
const rootCandidates = snapshot.roots.map(inspectRoot);
|
|
1829
1840
|
const initializedRoots = rootCandidates.filter(
|
|
1830
1841
|
(candidate) => candidate.initialized && candidate.path !== void 0
|
|
@@ -1952,11 +1963,16 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
|
|
|
1952
1963
|
if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
|
|
1953
1964
|
return fileURLToPath(parsed, { windows });
|
|
1954
1965
|
}
|
|
1955
|
-
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS) {
|
|
1966
|
+
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
|
|
1956
1967
|
if (!provider) return { supported: false, roots: [] };
|
|
1957
1968
|
try {
|
|
1958
|
-
return await withOperationTimeout(
|
|
1969
|
+
return await withOperationTimeout(
|
|
1970
|
+
"MCP Roots request",
|
|
1971
|
+
timeoutMs,
|
|
1972
|
+
() => provider(call)
|
|
1973
|
+
);
|
|
1959
1974
|
} catch (error) {
|
|
1975
|
+
if (error instanceof McpRootsPending) throw error;
|
|
1960
1976
|
return {
|
|
1961
1977
|
supported: true,
|
|
1962
1978
|
roots: [],
|
|
@@ -2034,7 +2050,7 @@ var TARGET_MCP_TOOL_NAMES = [
|
|
|
2034
2050
|
"support",
|
|
2035
2051
|
"report"
|
|
2036
2052
|
];
|
|
2037
|
-
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
2053
|
+
var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
|
|
2038
2054
|
schemaVersion: z.literal(1),
|
|
2039
2055
|
outcome: z.enum([
|
|
2040
2056
|
"completed",
|
|
@@ -2116,7 +2132,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
|
2116
2132
|
reasonCode: z.string().optional()
|
|
2117
2133
|
})
|
|
2118
2134
|
)
|
|
2119
|
-
};
|
|
2135
|
+
});
|
|
2120
2136
|
function structuredToolResult(envelope) {
|
|
2121
2137
|
const clientTimeZone = clientRuntimeTimeZone();
|
|
2122
2138
|
const presentation = {
|
|
@@ -2230,9 +2246,9 @@ function resolverFor(ctx) {
|
|
|
2230
2246
|
}
|
|
2231
2247
|
return resolver;
|
|
2232
2248
|
}
|
|
2233
|
-
async function withProjectDir(ctx) {
|
|
2249
|
+
async function withProjectDir(ctx, call) {
|
|
2234
2250
|
try {
|
|
2235
|
-
const binding = await resolverFor(ctx).resolve();
|
|
2251
|
+
const binding = await resolverFor(ctx).resolve(call);
|
|
2236
2252
|
const resolved = resolveLockedProjectRoot(binding.projectDir);
|
|
2237
2253
|
return {
|
|
2238
2254
|
...ctx,
|
|
@@ -2255,12 +2271,12 @@ async function withProjectDir(ctx) {
|
|
|
2255
2271
|
throw error;
|
|
2256
2272
|
}
|
|
2257
2273
|
}
|
|
2258
|
-
async function diagnoseProjectBinding(ctx) {
|
|
2259
|
-
return resolverFor(ctx).diagnose();
|
|
2274
|
+
async function diagnoseProjectBinding(ctx, call) {
|
|
2275
|
+
return resolverFor(ctx).diagnose(call);
|
|
2260
2276
|
}
|
|
2261
|
-
async function initializeWorkspaceProject(ctx) {
|
|
2277
|
+
async function initializeWorkspaceProject(ctx, call) {
|
|
2262
2278
|
try {
|
|
2263
|
-
const resolved = await resolverFor(ctx).initialize();
|
|
2279
|
+
const resolved = await resolverFor(ctx).initialize(call);
|
|
2264
2280
|
return {
|
|
2265
2281
|
...ctx,
|
|
2266
2282
|
projectDir: resolved.projectDir,
|
|
@@ -2276,10 +2292,11 @@ async function initializeWorkspaceProject(ctx) {
|
|
|
2276
2292
|
throw error;
|
|
2277
2293
|
}
|
|
2278
2294
|
}
|
|
2279
|
-
async function optionalProjectContext(ctx) {
|
|
2295
|
+
async function optionalProjectContext(ctx, call) {
|
|
2280
2296
|
try {
|
|
2281
|
-
return await withProjectDir(ctx);
|
|
2282
|
-
} catch {
|
|
2297
|
+
return await withProjectDir(ctx, call);
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
if (error instanceof McpRootsPending) throw error;
|
|
2283
2300
|
return null;
|
|
2284
2301
|
}
|
|
2285
2302
|
}
|
|
@@ -2327,6 +2344,9 @@ function requireSiteFile(ctx) {
|
|
|
2327
2344
|
}
|
|
2328
2345
|
var UNAUTHORIZED_SUMMARY = "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.";
|
|
2329
2346
|
function toolError(e) {
|
|
2347
|
+
if (e instanceof McpRootsPending) {
|
|
2348
|
+
return inputRequired({ inputRequests: { roots: inputRequired.listRoots() } });
|
|
2349
|
+
}
|
|
2330
2350
|
const isSakupa = isSakupaError(e);
|
|
2331
2351
|
const errorCode = isSakupa ? e.code : "internal";
|
|
2332
2352
|
const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
|
|
@@ -4146,13 +4166,13 @@ function registerTools(server, baseCtx) {
|
|
|
4146
4166
|
description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy.",
|
|
4147
4167
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4148
4168
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
4149
|
-
inputSchema: {
|
|
4169
|
+
inputSchema: z2.object({
|
|
4150
4170
|
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
4151
|
-
}
|
|
4171
|
+
})
|
|
4152
4172
|
},
|
|
4153
|
-
async (args) => {
|
|
4173
|
+
async (args, call) => {
|
|
4154
4174
|
try {
|
|
4155
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4175
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4156
4176
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
4157
4177
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
4158
4178
|
});
|
|
@@ -4173,7 +4193,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4173
4193
|
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
|
|
4174
4194
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4175
4195
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4176
|
-
inputSchema: {
|
|
4196
|
+
inputSchema: z2.object({
|
|
4177
4197
|
outputDir: z2.string().min(1).describe(
|
|
4178
4198
|
'REQUIRED: exact publish directory relative to the initialized project root, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). Sakupa applies it only inside the cwd-locked project.'
|
|
4179
4199
|
),
|
|
@@ -4199,12 +4219,12 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4199
4219
|
"Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
|
|
4200
4220
|
),
|
|
4201
4221
|
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
4202
|
-
}
|
|
4222
|
+
})
|
|
4203
4223
|
},
|
|
4204
|
-
async (args) => {
|
|
4224
|
+
async (args, call) => {
|
|
4205
4225
|
let releaseHandoffLock;
|
|
4206
4226
|
try {
|
|
4207
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4227
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4208
4228
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
4209
4229
|
if (!analysis.deployable || !analysis.files) {
|
|
4210
4230
|
return notDeployableResult(analysis);
|
|
@@ -4829,11 +4849,11 @@ Optional security recommendation: this management credential was created at ${ti
|
|
|
4829
4849
|
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscription-backed sites have no free-site expiry while the subscription remains active and need no refresh.",
|
|
4830
4850
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4831
4851
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4832
|
-
inputSchema: {}
|
|
4852
|
+
inputSchema: z2.object({})
|
|
4833
4853
|
},
|
|
4834
|
-
async () => {
|
|
4854
|
+
async (_args, call) => {
|
|
4835
4855
|
try {
|
|
4836
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4856
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4837
4857
|
const site = requireSiteFile(ctx);
|
|
4838
4858
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
4839
4859
|
if (site.url) {
|
|
@@ -4862,11 +4882,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4862
4882
|
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, custom domains, size, last deployment and warnings. For a paid site this tool also automatically returns the complete authoritative billing snapshot; users never need to know or name a separate billing tool to get accurate subscription information.",
|
|
4863
4883
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4864
4884
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
4865
|
-
inputSchema: {}
|
|
4885
|
+
inputSchema: z2.object({})
|
|
4866
4886
|
},
|
|
4867
|
-
async () => {
|
|
4887
|
+
async (_args, call) => {
|
|
4868
4888
|
try {
|
|
4869
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4889
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4870
4890
|
const site = requireSiteFile(ctx);
|
|
4871
4891
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
4872
4892
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -4893,15 +4913,15 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4893
4913
|
description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 24-hour expiry. Binding a custom domain afterwards (bind) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy first. If the site outgrows its plan, Sakupa shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
|
|
4894
4914
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4895
4915
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4896
|
-
inputSchema: {
|
|
4916
|
+
inputSchema: z2.object({
|
|
4897
4917
|
plan: planEnum.describe(
|
|
4898
4918
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
4899
4919
|
)
|
|
4900
|
-
}
|
|
4920
|
+
})
|
|
4901
4921
|
},
|
|
4902
|
-
async (args) => {
|
|
4922
|
+
async (args, call) => {
|
|
4903
4923
|
try {
|
|
4904
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4924
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4905
4925
|
const site = requireSiteFile(ctx);
|
|
4906
4926
|
const res = await ctx.client.createPlanCheckout(
|
|
4907
4927
|
{
|
|
@@ -4939,17 +4959,17 @@ Once Stripe confirms payment and Sakupa synchronizes the subscription, the curre
|
|
|
4939
4959
|
description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the subscription-backed ${previewHostPattern} URL keeps working alongside it while the subscription is active. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. One apex TXT verification covers both. A site has one FINAL apex domain; starting a different apex begins a zero-downtime switch and the previous domain remains until the new www is live. The www CNAME must remain while bound. Requires an ACTIVE subscription (subscribe). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and subscription-backed Sakupa URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
|
|
4940
4960
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4941
4961
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4942
|
-
inputSchema: {
|
|
4962
|
+
inputSchema: z2.object({
|
|
4943
4963
|
action: z2.enum(["start", "status"]),
|
|
4944
4964
|
hostname: z2.string().optional().describe("Required for start."),
|
|
4945
4965
|
verificationId: z2.string().optional().describe(
|
|
4946
4966
|
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
4947
4967
|
)
|
|
4948
|
-
}
|
|
4968
|
+
})
|
|
4949
4969
|
},
|
|
4950
|
-
async (args) => {
|
|
4970
|
+
async (args, call) => {
|
|
4951
4971
|
try {
|
|
4952
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4972
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4953
4973
|
const site = requireSiteFile(ctx);
|
|
4954
4974
|
if (args.action === "status") {
|
|
4955
4975
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -5078,11 +5098,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
5078
5098
|
description: "Return the sole authoritative source for this site's hosting subscription: current plan, next renewal plan or cancellation, effective time, payment state, current paid entitlement, reconciled paid usage or current free-site fair-use telemetry, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
|
|
5079
5099
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5080
5100
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
5081
|
-
inputSchema: {}
|
|
5101
|
+
inputSchema: z2.object({})
|
|
5082
5102
|
},
|
|
5083
|
-
async () => {
|
|
5103
|
+
async (_args, call) => {
|
|
5084
5104
|
try {
|
|
5085
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5105
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5086
5106
|
const site = requireSiteFile(ctx);
|
|
5087
5107
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
5088
5108
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -5122,14 +5142,14 @@ Full status:`, res);
|
|
|
5122
5142
|
description: "Open the Stripe-hosted billing portal for this site: update the payment method, view invoices, or cancel the subscription. All billing operations happen on the Stripe-hosted page \u2014 never inside the AI tool. With .sakupa/site.json, this opens the site-specific portal. Without the local credential, this returns Stripe's public no-code Customer Portal login page. The customer enters the checkout email and confirms a one-time passcode sent by Stripe. This never restores Sakupa site authority.",
|
|
5123
5143
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5124
5144
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5125
|
-
inputSchema: {
|
|
5145
|
+
inputSchema: z2.object({
|
|
5126
5146
|
scope: z2.enum(["site", "public_recovery"])
|
|
5127
|
-
}
|
|
5147
|
+
})
|
|
5128
5148
|
},
|
|
5129
|
-
async (args) => {
|
|
5149
|
+
async (args, call) => {
|
|
5130
5150
|
try {
|
|
5131
5151
|
if (args.scope === "site") {
|
|
5132
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5152
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5133
5153
|
const site = requireSiteFile(ctx);
|
|
5134
5154
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
5135
5155
|
return structuredToolResult({
|
|
@@ -5178,7 +5198,7 @@ Full status:`, res);
|
|
|
5178
5198
|
description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Recovery is resumable: start stores local pending state; complete installs and writes the new .sakupa/site.json credential BEFORE requesting content; download uses that credential to reissue an archive and safely extract it into the explicitly selected outputDir without repeating DNS.",
|
|
5179
5199
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5180
5200
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
5181
|
-
inputSchema: {
|
|
5201
|
+
inputSchema: z2.object({
|
|
5182
5202
|
action: z2.enum(["start", "status", "complete", "download"]),
|
|
5183
5203
|
hostname: z2.string().optional().describe("Required for start."),
|
|
5184
5204
|
verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
@@ -5186,11 +5206,11 @@ Full status:`, res);
|
|
|
5186
5206
|
"REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
|
|
5187
5207
|
),
|
|
5188
5208
|
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
5189
|
-
}
|
|
5209
|
+
})
|
|
5190
5210
|
},
|
|
5191
|
-
async (args) => {
|
|
5211
|
+
async (args, call) => {
|
|
5192
5212
|
try {
|
|
5193
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5213
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5194
5214
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
5195
5215
|
throw new LocalGuidanceError(
|
|
5196
5216
|
"invalid_request",
|
|
@@ -5539,16 +5559,16 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5539
5559
|
description: "Create a Sakupa support ticket for billing, payment, refund review, domain verification, deployment, serving or other issues the MCP cannot solve automatically. Do not include secrets, credentials or card data in the description.",
|
|
5540
5560
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5541
5561
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5542
|
-
inputSchema: {
|
|
5562
|
+
inputSchema: z2.object({
|
|
5543
5563
|
category: ticketCategoryEnum,
|
|
5544
5564
|
subject: z2.string().describe("Short subject line."),
|
|
5545
5565
|
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
5546
5566
|
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
5547
|
-
}
|
|
5567
|
+
})
|
|
5548
5568
|
},
|
|
5549
|
-
async (args) => {
|
|
5569
|
+
async (args, call) => {
|
|
5550
5570
|
try {
|
|
5551
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5571
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5552
5572
|
const site = requireSiteFile(ctx);
|
|
5553
5573
|
const res = await ctx.client.createTicket(site.credential, {
|
|
5554
5574
|
siteId: site.siteId,
|
|
@@ -5573,7 +5593,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5573
5593
|
description: "LAST RESORT after help explicitly returns reportRecommended:true. Prepare and submit a sanitized product bug report using helpAuthorization from that diagnosis. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
|
|
5574
5594
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5575
5595
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5576
|
-
inputSchema: {
|
|
5596
|
+
inputSchema: z2.object({
|
|
5577
5597
|
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
5578
5598
|
helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
|
|
5579
5599
|
errorCode: z2.string().optional(),
|
|
@@ -5589,12 +5609,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5589
5609
|
"OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
|
|
5590
5610
|
),
|
|
5591
5611
|
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
5592
|
-
}
|
|
5612
|
+
})
|
|
5593
5613
|
},
|
|
5594
|
-
async (args) => {
|
|
5614
|
+
async (args, call) => {
|
|
5595
5615
|
try {
|
|
5596
5616
|
requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
|
|
5597
|
-
const ctx = await optionalProjectContext(baseCtx);
|
|
5617
|
+
const ctx = await optionalProjectContext(baseCtx, call);
|
|
5598
5618
|
const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
|
|
5599
5619
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
5600
5620
|
const diagnostics = {
|
|
@@ -5668,7 +5688,11 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
5668
5688
|
}
|
|
5669
5689
|
|
|
5670
5690
|
// src/server.ts
|
|
5671
|
-
import {
|
|
5691
|
+
import {
|
|
5692
|
+
CLIENT_CAPABILITIES_META_KEY,
|
|
5693
|
+
McpServer,
|
|
5694
|
+
inputResponse
|
|
5695
|
+
} from "@modelcontextprotocol/server";
|
|
5672
5696
|
|
|
5673
5697
|
// src/tools/billing.ts
|
|
5674
5698
|
import { z as z3 } from "zod";
|
|
@@ -5677,11 +5701,11 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5677
5701
|
"plans",
|
|
5678
5702
|
{
|
|
5679
5703
|
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.",
|
|
5680
|
-
inputSchema: {},
|
|
5704
|
+
inputSchema: z3.object({}),
|
|
5681
5705
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5682
5706
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
5683
5707
|
},
|
|
5684
|
-
async () => {
|
|
5708
|
+
async (_args, call) => {
|
|
5685
5709
|
try {
|
|
5686
5710
|
const catalog = await baseCtx.client.getBillingPlanCatalog();
|
|
5687
5711
|
return structuredToolResult({
|
|
@@ -5701,15 +5725,15 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5701
5725
|
"change",
|
|
5702
5726
|
{
|
|
5703
5727
|
description: "Create one Stripe-hosted subscription-management link. The user chooses the plan or period-end cancellation on Stripe; Sakupa never infers intent from the conversation. Creating the link does not change billing.",
|
|
5704
|
-
inputSchema: {
|
|
5728
|
+
inputSchema: z3.object({
|
|
5705
5729
|
operationId: z3.string().min(1)
|
|
5706
|
-
},
|
|
5730
|
+
}),
|
|
5707
5731
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5708
5732
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
5709
5733
|
},
|
|
5710
|
-
async (args) => {
|
|
5734
|
+
async (args, call) => {
|
|
5711
5735
|
try {
|
|
5712
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5736
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5713
5737
|
const site = requireSiteFile(ctx);
|
|
5714
5738
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
5715
5739
|
siteId: site.siteId,
|
|
@@ -5992,13 +6016,13 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5992
6016
|
"init",
|
|
5993
6017
|
{
|
|
5994
6018
|
description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. If MCP Roots are unavailable, call help; the AI may then use the no-argument CLI init itself.",
|
|
5995
|
-
inputSchema: {},
|
|
6019
|
+
inputSchema: z4.object({}),
|
|
5996
6020
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5997
6021
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
5998
6022
|
},
|
|
5999
|
-
async () => {
|
|
6023
|
+
async (_args, call) => {
|
|
6000
6024
|
try {
|
|
6001
|
-
const ctx = await initializeWorkspaceProject(baseCtx);
|
|
6025
|
+
const ctx = await initializeWorkspaceProject(baseCtx, call);
|
|
6002
6026
|
const marker = loadProjectMarker(ctx.projectDir);
|
|
6003
6027
|
if (marker.kind !== "ok")
|
|
6004
6028
|
throw new Error("init postcondition failed: project marker missing");
|
|
@@ -6032,17 +6056,17 @@ function registerHelpTools(server, baseCtx) {
|
|
|
6032
6056
|
"help",
|
|
6033
6057
|
{
|
|
6034
6058
|
description: "FIRST troubleshooting tool for every Sakupa difficulty. With topic diagnose (default), inspect MCP Roots, cwd, binding and local state without requiring a project or calling the API. Use overview, terminology, or a tool name for complete usage, side effects, parameters and warnings. Only recommend report when help explicitly returns reportRecommended:true.",
|
|
6035
|
-
inputSchema: {
|
|
6059
|
+
inputSchema: z4.object({
|
|
6036
6060
|
topic: z4.enum(HELP_TOPICS).optional().default("diagnose"),
|
|
6037
6061
|
failedTool: z4.string().optional(),
|
|
6038
6062
|
errorCode: z4.string().optional(),
|
|
6039
6063
|
resultCode: z4.string().optional(),
|
|
6040
6064
|
requestId: z4.string().optional()
|
|
6041
|
-
},
|
|
6065
|
+
}),
|
|
6042
6066
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
6043
6067
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
6044
6068
|
},
|
|
6045
|
-
async (args) => {
|
|
6069
|
+
async (args, call) => {
|
|
6046
6070
|
try {
|
|
6047
6071
|
if (args.topic === "overview") {
|
|
6048
6072
|
const catalog = Object.fromEntries(
|
|
@@ -6104,7 +6128,7 @@ Terminology: ${terminologyText}` : ""),
|
|
|
6104
6128
|
nextActions: []
|
|
6105
6129
|
});
|
|
6106
6130
|
}
|
|
6107
|
-
const diagnosis = await diagnoseProjectBinding(baseCtx);
|
|
6131
|
+
const diagnosis = await diagnoseProjectBinding(baseCtx, call);
|
|
6108
6132
|
const selected = diagnosis.selectedProjectDir;
|
|
6109
6133
|
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
6110
6134
|
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
@@ -6183,18 +6207,18 @@ function registerCredentialTools(server, baseCtx) {
|
|
|
6183
6207
|
"rotate",
|
|
6184
6208
|
{
|
|
6185
6209
|
description: "Optionally rotate this site management credential. The first call is a read-only preview. Only confirmed:true after explicit user approval installs a locally generated new credential and revokes every previous credential. Rotation is never required to deploy.",
|
|
6186
|
-
inputSchema: {
|
|
6210
|
+
inputSchema: z5.object({
|
|
6187
6211
|
confirmed: z5.boolean().optional().describe(
|
|
6188
6212
|
"True only after showing the rotate preview and the user explicitly approves revoking every old credential."
|
|
6189
6213
|
)
|
|
6190
|
-
},
|
|
6214
|
+
}),
|
|
6191
6215
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
6192
6216
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
6193
6217
|
},
|
|
6194
|
-
async (args) => {
|
|
6218
|
+
async (args, call) => {
|
|
6195
6219
|
let releaseLock;
|
|
6196
6220
|
try {
|
|
6197
|
-
const ctx = await withProjectDir(baseCtx);
|
|
6221
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
6198
6222
|
let site = requireSiteFile(ctx);
|
|
6199
6223
|
const pending = loadCredentialRotation(ctx.projectDir);
|
|
6200
6224
|
if (pending.kind !== "absent" || args.confirmed === true) {
|
|
@@ -6434,23 +6458,7 @@ function createSakupaMcpServer(opts) {
|
|
|
6434
6458
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
6435
6459
|
);
|
|
6436
6460
|
const processCwd = resolve6(opts.projectDir ?? process.cwd());
|
|
6437
|
-
const rootsProvider = opts.rootsProvider ?? (
|
|
6438
|
-
const capabilities = server.server.getClientCapabilities();
|
|
6439
|
-
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
6440
|
-
try {
|
|
6441
|
-
const response = await server.server.listRoots(void 0, {
|
|
6442
|
-
timeout: MCP_ROOTS_TIMEOUT_MS,
|
|
6443
|
-
maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
|
|
6444
|
-
});
|
|
6445
|
-
return { supported: true, roots: response.roots };
|
|
6446
|
-
} catch (error) {
|
|
6447
|
-
return {
|
|
6448
|
-
supported: true,
|
|
6449
|
-
roots: [],
|
|
6450
|
-
error: error instanceof Error ? error.message : String(error)
|
|
6451
|
-
};
|
|
6452
|
-
}
|
|
6453
|
-
});
|
|
6461
|
+
const rootsProvider = opts.rootsProvider ?? ((call) => readClientRoots(server, call));
|
|
6454
6462
|
const ctx = {
|
|
6455
6463
|
client,
|
|
6456
6464
|
apiBaseUrl: opts.apiBaseUrl,
|
|
@@ -6464,6 +6472,38 @@ function createSakupaMcpServer(opts) {
|
|
|
6464
6472
|
registerHelpTools(server, ctx);
|
|
6465
6473
|
return server;
|
|
6466
6474
|
}
|
|
6475
|
+
async function readClientRoots(server, call) {
|
|
6476
|
+
if (call?.mcpReq.envelope !== void 0) {
|
|
6477
|
+
const envelope = call.mcpReq.envelope;
|
|
6478
|
+
const declared = envelope[CLIENT_CAPABILITIES_META_KEY];
|
|
6479
|
+
if (!declared?.roots) return { supported: false, roots: [] };
|
|
6480
|
+
const answered = inputResponse(call.mcpReq.inputResponses, "roots");
|
|
6481
|
+
if (answered.kind === "roots") return { supported: true, roots: answered.roots };
|
|
6482
|
+
if (call.mcpReq.inputResponses !== void 0) {
|
|
6483
|
+
return {
|
|
6484
|
+
supported: true,
|
|
6485
|
+
roots: [],
|
|
6486
|
+
error: "The client retried without answering the embedded roots/list request."
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
throw new McpRootsPending();
|
|
6490
|
+
}
|
|
6491
|
+
const capabilities = server.server.getClientCapabilities();
|
|
6492
|
+
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
6493
|
+
try {
|
|
6494
|
+
const response = await server.server.listRoots(void 0, {
|
|
6495
|
+
timeout: MCP_ROOTS_TIMEOUT_MS,
|
|
6496
|
+
maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
|
|
6497
|
+
});
|
|
6498
|
+
return { supported: true, roots: response.roots };
|
|
6499
|
+
} catch (error) {
|
|
6500
|
+
return {
|
|
6501
|
+
supported: true,
|
|
6502
|
+
roots: [],
|
|
6503
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6504
|
+
};
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6467
6507
|
export {
|
|
6468
6508
|
CLIENT_TYPE,
|
|
6469
6509
|
FetchTransport,
|
package/package.json
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sakupa/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"mcpName": "io.github.myerwang/sakupa",
|
|
4
5
|
"description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
|
|
6
|
+
"homepage": "https://sakupa.com/manual/",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"mcp-server",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"static-site",
|
|
12
|
+
"deploy",
|
|
13
|
+
"hosting",
|
|
14
|
+
"sakupa"
|
|
15
|
+
],
|
|
5
16
|
"type": "module",
|
|
6
17
|
"main": "dist/index.js",
|
|
7
18
|
"bin": {
|
|
@@ -23,10 +34,11 @@
|
|
|
23
34
|
"node": ">=20"
|
|
24
35
|
},
|
|
25
36
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/
|
|
27
|
-
"zod": "^
|
|
37
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
38
|
+
"zod": "^4.2.0"
|
|
28
39
|
},
|
|
29
40
|
"devDependencies": {
|
|
41
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
30
42
|
"esbuild": "^0.21.5",
|
|
31
43
|
"fflate": "0.8.3"
|
|
32
44
|
}
|