@sakupa/mcp 1.0.0 → 1.2.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 +253 -163
- package/dist/index.js +248 -163
- 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 = "1.
|
|
405
|
+
var SAKUPA_MCP_VERSION = "1.2.0";
|
|
406
406
|
|
|
407
407
|
// ../core/dist/domain/errors.js
|
|
408
408
|
var HTTP_STATUS = {
|
|
@@ -738,13 +738,15 @@ function previewHostPatternFor(apiBaseUrl) {
|
|
|
738
738
|
function loadMcpRuntimeConfig(env = process.env) {
|
|
739
739
|
const apiBaseUrl = (env["SAKUPA_API_URL"] ?? env["SAKUPA_API_BASE_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
740
740
|
const testAccessToken = env["SAKUPA_TEST_ACCESS_TOKEN"]?.trim() ?? "";
|
|
741
|
+
const projectRoot = env["SAKUPA_PROJECT_ROOT"]?.trim() ?? "";
|
|
742
|
+
const projectRootConfig = projectRoot.length > 0 ? { projectRoot } : {};
|
|
741
743
|
if (apiBaseUrl === TEST_API_BASE_URL) {
|
|
742
744
|
if (testAccessToken.length === 0) {
|
|
743
745
|
throw new Error(
|
|
744
746
|
"The Sakupa Test API requires SAKUPA_TEST_ACCESS_TOKEN. Anonymous Test access is disabled."
|
|
745
747
|
);
|
|
746
748
|
}
|
|
747
|
-
return { apiBaseUrl, testAccessToken };
|
|
749
|
+
return { apiBaseUrl, testAccessToken, ...projectRootConfig };
|
|
748
750
|
}
|
|
749
751
|
if (apiBaseUrl !== PRODUCTION_API_BASE_URL) {
|
|
750
752
|
throw new Error(
|
|
@@ -756,7 +758,7 @@ function loadMcpRuntimeConfig(env = process.env) {
|
|
|
756
758
|
`SAKUPA_TEST_ACCESS_TOKEN may only be used with ${TEST_API_BASE_URL}. Remove it before connecting to any other API.`
|
|
757
759
|
);
|
|
758
760
|
}
|
|
759
|
-
return { apiBaseUrl };
|
|
761
|
+
return { apiBaseUrl, ...projectRootConfig };
|
|
760
762
|
}
|
|
761
763
|
function environmentFor(apiBaseUrl) {
|
|
762
764
|
if (apiBaseUrl === TEST_API_BASE_URL) return "test";
|
|
@@ -765,7 +767,11 @@ function environmentFor(apiBaseUrl) {
|
|
|
765
767
|
}
|
|
766
768
|
|
|
767
769
|
// src/server.ts
|
|
768
|
-
import {
|
|
770
|
+
import {
|
|
771
|
+
CLIENT_CAPABILITIES_META_KEY,
|
|
772
|
+
McpServer,
|
|
773
|
+
inputResponse
|
|
774
|
+
} from "@modelcontextprotocol/server";
|
|
769
775
|
|
|
770
776
|
// src/api-client.ts
|
|
771
777
|
var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
@@ -2946,10 +2952,21 @@ async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
|
|
|
2946
2952
|
};
|
|
2947
2953
|
}
|
|
2948
2954
|
|
|
2955
|
+
// src/tools/context.ts
|
|
2956
|
+
import {
|
|
2957
|
+
inputRequired
|
|
2958
|
+
} from "@modelcontextprotocol/server";
|
|
2959
|
+
|
|
2949
2960
|
// src/project-binding.ts
|
|
2950
2961
|
import { fileURLToPath } from "node:url";
|
|
2951
|
-
import { resolve as resolve4 } from "node:path";
|
|
2962
|
+
import { isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
|
|
2952
2963
|
var MCP_ROOTS_TIMEOUT_MS = 5e3;
|
|
2964
|
+
var McpRootsPending = class extends Error {
|
|
2965
|
+
constructor() {
|
|
2966
|
+
super("MCP Roots must be requested from the client before the project can be bound.");
|
|
2967
|
+
this.name = "McpRootsPending";
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2953
2970
|
var ProjectBindingError = class extends Error {
|
|
2954
2971
|
diagnostics;
|
|
2955
2972
|
constructor(diagnostics) {
|
|
@@ -2959,18 +2976,19 @@ var ProjectBindingError = class extends Error {
|
|
|
2959
2976
|
}
|
|
2960
2977
|
};
|
|
2961
2978
|
var ProjectBindingResolver = class {
|
|
2962
|
-
constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS) {
|
|
2979
|
+
constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS, configuredRoot) {
|
|
2963
2980
|
this.processCwd = processCwd;
|
|
2964
2981
|
this.rootsProvider = rootsProvider;
|
|
2965
2982
|
this.rootsTimeoutMs = rootsTimeoutMs;
|
|
2983
|
+
this.configuredRoot = configuredRoot;
|
|
2966
2984
|
}
|
|
2967
2985
|
bound;
|
|
2968
2986
|
boundState;
|
|
2969
2987
|
resolving;
|
|
2970
|
-
async resolve() {
|
|
2988
|
+
async resolve(call) {
|
|
2971
2989
|
if (this.bound) return this.bound;
|
|
2972
2990
|
if (this.resolving) return this.resolving;
|
|
2973
|
-
this.resolving = this.inspect().then((inspection) => {
|
|
2991
|
+
this.resolving = this.inspect(false, call).then((inspection) => {
|
|
2974
2992
|
if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
|
|
2975
2993
|
this.bound = inspection.selected;
|
|
2976
2994
|
this.boundState = inspection.diagnostics;
|
|
@@ -2980,18 +2998,18 @@ var ProjectBindingResolver = class {
|
|
|
2980
2998
|
});
|
|
2981
2999
|
return this.resolving;
|
|
2982
3000
|
}
|
|
2983
|
-
async diagnose() {
|
|
3001
|
+
async diagnose(call) {
|
|
2984
3002
|
if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
|
|
2985
|
-
const inspection = await this.inspect();
|
|
3003
|
+
const inspection = await this.inspect(false, call);
|
|
2986
3004
|
if (inspection.selected) {
|
|
2987
3005
|
this.bound = inspection.selected;
|
|
2988
3006
|
this.boundState = inspection.diagnostics;
|
|
2989
3007
|
}
|
|
2990
3008
|
return inspection.diagnostics;
|
|
2991
3009
|
}
|
|
2992
|
-
async initialize() {
|
|
3010
|
+
async initialize(call) {
|
|
2993
3011
|
if (this.bound) return this.bound;
|
|
2994
|
-
const inspection = await this.inspect(true);
|
|
3012
|
+
const inspection = await this.inspect(true, call);
|
|
2995
3013
|
if (inspection.selected) {
|
|
2996
3014
|
this.bound = inspection.selected;
|
|
2997
3015
|
this.boundState = inspection.diagnostics;
|
|
@@ -3001,28 +3019,41 @@ var ProjectBindingResolver = class {
|
|
|
3001
3019
|
throw new ProjectBindingError(inspection.diagnostics);
|
|
3002
3020
|
}
|
|
3003
3021
|
const initialized = initializeProject(inspection.initializableRoot);
|
|
3004
|
-
this.bound = {
|
|
3022
|
+
this.bound = {
|
|
3023
|
+
...initialized,
|
|
3024
|
+
bindingSource: inspection.initializableSource ?? "mcp_root"
|
|
3025
|
+
};
|
|
3005
3026
|
this.boundState = boundDiagnostics(
|
|
3006
3027
|
this.processCwd,
|
|
3007
3028
|
this.bound,
|
|
3008
|
-
{ supported:
|
|
3009
|
-
inspection.diagnostics.rootCandidates
|
|
3029
|
+
{ supported: inspection.diagnostics.mcpRootsSupported, roots: [] },
|
|
3030
|
+
inspection.diagnostics.rootCandidates,
|
|
3031
|
+
inspection.diagnostics.configuredProjectRoot
|
|
3010
3032
|
);
|
|
3011
3033
|
return this.bound;
|
|
3012
3034
|
}
|
|
3013
|
-
async inspect(forInitialization = false) {
|
|
3014
|
-
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs);
|
|
3035
|
+
async inspect(forInitialization = false, call) {
|
|
3036
|
+
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
|
|
3015
3037
|
const rootCandidates = snapshot.roots.map(inspectRoot);
|
|
3038
|
+
const configured = this.configuredRoot === void 0 ? void 0 : inspectConfiguredRoot(this.configuredRoot);
|
|
3039
|
+
const diagnose = (code, guidance) => diagnostic(code, snapshot, this.processCwd, rootCandidates, guidance, configured);
|
|
3040
|
+
const bound = (selected) => ({
|
|
3041
|
+
selected,
|
|
3042
|
+
diagnostics: boundDiagnostics(
|
|
3043
|
+
this.processCwd,
|
|
3044
|
+
selected,
|
|
3045
|
+
snapshot,
|
|
3046
|
+
rootCandidates,
|
|
3047
|
+
configured
|
|
3048
|
+
)
|
|
3049
|
+
});
|
|
3016
3050
|
const initializedRoots = rootCandidates.filter(
|
|
3017
3051
|
(candidate) => candidate.initialized && candidate.path !== void 0
|
|
3018
3052
|
);
|
|
3019
3053
|
if (snapshot.supported && snapshot.error) {
|
|
3020
3054
|
return {
|
|
3021
|
-
diagnostics:
|
|
3055
|
+
diagnostics: diagnose(
|
|
3022
3056
|
"roots_request_failed",
|
|
3023
|
-
snapshot,
|
|
3024
|
-
this.processCwd,
|
|
3025
|
-
rootCandidates,
|
|
3026
3057
|
"The IDE advertised MCP Roots, but the Roots request failed. Retry help after the IDE finishes loading the workspace. If it persists, restart the MCP connection; do not initialize or deploy from the IDE installation directory."
|
|
3027
3058
|
)
|
|
3028
3059
|
};
|
|
@@ -3031,19 +3062,12 @@ var ProjectBindingResolver = class {
|
|
|
3031
3062
|
const initializedRoot = initializedRoots[0];
|
|
3032
3063
|
if (!initializedRoot) throw new Error("initialized Root disappeared during resolution");
|
|
3033
3064
|
const project = resolveLockedProjectRoot(initializedRoot.path);
|
|
3034
|
-
|
|
3035
|
-
return {
|
|
3036
|
-
selected,
|
|
3037
|
-
diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
|
|
3038
|
-
};
|
|
3065
|
+
return bound({ ...project, bindingSource: "mcp_root" });
|
|
3039
3066
|
}
|
|
3040
3067
|
if (initializedRoots.length > 1) {
|
|
3041
3068
|
return {
|
|
3042
|
-
diagnostics:
|
|
3069
|
+
diagnostics: diagnose(
|
|
3043
3070
|
"multiple_initialized_roots",
|
|
3044
|
-
snapshot,
|
|
3045
|
-
this.processCwd,
|
|
3046
|
-
rootCandidates,
|
|
3047
3071
|
"More than one IDE workspace Root is already initialized for Sakupa. Close the unrelated workspaces and retry help; Sakupa will not guess which site to manage."
|
|
3048
3072
|
)
|
|
3049
3073
|
};
|
|
@@ -3057,22 +3081,17 @@ var ProjectBindingResolver = class {
|
|
|
3057
3081
|
if (!validRoot) throw new Error("workspace Root disappeared during initialization");
|
|
3058
3082
|
return {
|
|
3059
3083
|
initializableRoot: validRoot.path,
|
|
3060
|
-
|
|
3084
|
+
initializableSource: "mcp_root",
|
|
3085
|
+
diagnostics: diagnose(
|
|
3061
3086
|
"workspace_not_initialized",
|
|
3062
|
-
snapshot,
|
|
3063
|
-
this.processCwd,
|
|
3064
|
-
rootCandidates,
|
|
3065
3087
|
`The active MCP workspace ${validRoot.path} is ready to initialize.`
|
|
3066
3088
|
)
|
|
3067
3089
|
};
|
|
3068
3090
|
}
|
|
3069
3091
|
if (validRoots.length > 1) {
|
|
3070
3092
|
return {
|
|
3071
|
-
diagnostics:
|
|
3093
|
+
diagnostics: diagnose(
|
|
3072
3094
|
"multiple_uninitialized_roots",
|
|
3073
|
-
snapshot,
|
|
3074
|
-
this.processCwd,
|
|
3075
|
-
rootCandidates,
|
|
3076
3095
|
"The IDE exposes multiple uninitialized workspace Roots. Open only the intended project before calling init; Sakupa will not choose a directory for the user."
|
|
3077
3096
|
)
|
|
3078
3097
|
};
|
|
@@ -3082,54 +3101,59 @@ var ProjectBindingResolver = class {
|
|
|
3082
3101
|
const validRoot = validRoots[0];
|
|
3083
3102
|
if (!validRoot) throw new Error("workspace Root disappeared during diagnosis");
|
|
3084
3103
|
return {
|
|
3085
|
-
diagnostics:
|
|
3104
|
+
diagnostics: diagnose(
|
|
3086
3105
|
"workspace_not_initialized",
|
|
3087
|
-
snapshot,
|
|
3088
|
-
this.processCwd,
|
|
3089
|
-
rootCandidates,
|
|
3090
3106
|
`The IDE workspace ${validRoot.path} is not initialized. Call init with no path arguments; it will create .sakupa directly in that workspace Root.`
|
|
3091
3107
|
)
|
|
3092
3108
|
};
|
|
3093
3109
|
}
|
|
3094
3110
|
if (snapshot.supported && validRoots.length > 1) {
|
|
3095
3111
|
return {
|
|
3096
|
-
diagnostics:
|
|
3112
|
+
diagnostics: diagnose(
|
|
3097
3113
|
"multiple_uninitialized_roots",
|
|
3098
|
-
snapshot,
|
|
3099
|
-
this.processCwd,
|
|
3100
|
-
rootCandidates,
|
|
3101
3114
|
"The IDE exposes multiple uninitialized workspace Roots. Open only the intended project, then call init. Sakupa will not guess a project directory."
|
|
3102
3115
|
)
|
|
3103
3116
|
};
|
|
3104
3117
|
}
|
|
3118
|
+
if (configured) {
|
|
3119
|
+
if (configured.problem !== void 0 || configured.path === void 0) {
|
|
3120
|
+
return {
|
|
3121
|
+
diagnostics: diagnose(
|
|
3122
|
+
"invalid_configured_root",
|
|
3123
|
+
`SAKUPA_PROJECT_ROOT is set to ${configured.configured} but it is not usable: ${configured.problem ?? "unknown problem"}. Fix the MCP server configuration (an absolute path to an existing project directory) and retry help; Sakupa will not fall back to another directory.`
|
|
3124
|
+
)
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
if (configured.initialized) {
|
|
3128
|
+
const project = resolveLockedProjectRoot(configured.path);
|
|
3129
|
+
return bound({ ...project, bindingSource: "configured_root" });
|
|
3130
|
+
}
|
|
3131
|
+
return {
|
|
3132
|
+
...forInitialization ? { initializableRoot: configured.path, initializableSource: "configured_root" } : {},
|
|
3133
|
+
diagnostics: diagnose(
|
|
3134
|
+
"workspace_not_initialized",
|
|
3135
|
+
`The configured project root ${configured.path} (SAKUPA_PROJECT_ROOT) is not initialized. Call init with no path arguments; it will create .sakupa directly there.`
|
|
3136
|
+
)
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3105
3139
|
if (snapshot.supported) {
|
|
3106
3140
|
return {
|
|
3107
|
-
diagnostics:
|
|
3141
|
+
diagnostics: diagnose(
|
|
3108
3142
|
"workspace_not_initialized",
|
|
3109
|
-
snapshot,
|
|
3110
|
-
this.processCwd,
|
|
3111
|
-
rootCandidates,
|
|
3112
3143
|
"The IDE did not expose one usable file workspace Root. Open exactly one local project workspace, then retry help before calling init or deploy."
|
|
3113
3144
|
)
|
|
3114
3145
|
};
|
|
3115
3146
|
}
|
|
3116
3147
|
try {
|
|
3117
3148
|
const cwdProject = resolveLockedProjectRoot(this.processCwd);
|
|
3118
|
-
|
|
3119
|
-
return {
|
|
3120
|
-
selected,
|
|
3121
|
-
diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
|
|
3122
|
-
};
|
|
3149
|
+
return bound({ ...cwdProject, bindingSource: "process_cwd" });
|
|
3123
3150
|
} catch {
|
|
3124
3151
|
}
|
|
3125
3152
|
const cwdProblem = inspectDirectory(this.processCwd);
|
|
3126
3153
|
return {
|
|
3127
|
-
diagnostics:
|
|
3154
|
+
diagnostics: diagnose(
|
|
3128
3155
|
cwdProblem.problem ? "invalid_process_cwd" : "process_cwd_is_not_workspace",
|
|
3129
|
-
|
|
3130
|
-
this.processCwd,
|
|
3131
|
-
rootCandidates,
|
|
3132
|
-
"This IDE did not provide MCP Roots and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run help from the intended project context. If help confirms the missing-Roots diagnosis, the AI may run `npx -y @sakupa/mcp@latest init` with no path arguments from that directory. Never ask the user to run it."
|
|
3156
|
+
"This IDE did not provide MCP Roots, SAKUPA_PROJECT_ROOT is not configured, and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run help from the intended project context. If help confirms the missing-Roots diagnosis, the AI may run `npx -y @sakupa/mcp@latest init` with no path arguments from that directory, or the MCP server configuration may set SAKUPA_PROJECT_ROOT to the absolute project path. Never ask the user to run it."
|
|
3133
3157
|
)
|
|
3134
3158
|
};
|
|
3135
3159
|
}
|
|
@@ -3139,11 +3163,12 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
|
|
|
3139
3163
|
if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
|
|
3140
3164
|
return fileURLToPath(parsed, { windows });
|
|
3141
3165
|
}
|
|
3142
|
-
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS) {
|
|
3166
|
+
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
|
|
3143
3167
|
if (!provider) return { supported: false, roots: [] };
|
|
3144
3168
|
try {
|
|
3145
|
-
return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider());
|
|
3169
|
+
return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider(call));
|
|
3146
3170
|
} catch (error) {
|
|
3171
|
+
if (error instanceof McpRootsPending) throw error;
|
|
3147
3172
|
return {
|
|
3148
3173
|
supported: true,
|
|
3149
3174
|
roots: [],
|
|
@@ -3171,6 +3196,31 @@ function inspectRoot(root) {
|
|
|
3171
3196
|
};
|
|
3172
3197
|
}
|
|
3173
3198
|
}
|
|
3199
|
+
function inspectConfiguredRoot(configured) {
|
|
3200
|
+
if (!isAbsolute4(configured)) {
|
|
3201
|
+
return {
|
|
3202
|
+
configured,
|
|
3203
|
+
initialized: false,
|
|
3204
|
+
problem: "the value must be an absolute path"
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
try {
|
|
3208
|
+
const path = canonicalProjectDirectory(configured);
|
|
3209
|
+
const marker = loadProjectMarker(path);
|
|
3210
|
+
return {
|
|
3211
|
+
configured,
|
|
3212
|
+
path,
|
|
3213
|
+
initialized: marker.kind === "ok",
|
|
3214
|
+
...marker.kind === "corrupted" ? { problem: marker.problem } : {}
|
|
3215
|
+
};
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
return {
|
|
3218
|
+
configured,
|
|
3219
|
+
initialized: false,
|
|
3220
|
+
problem: error instanceof Error ? error.message : String(error)
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3174
3224
|
function inspectDirectory(path) {
|
|
3175
3225
|
try {
|
|
3176
3226
|
return { path: canonicalProjectDirectory(resolve4(path)) };
|
|
@@ -3178,22 +3228,24 @@ function inspectDirectory(path) {
|
|
|
3178
3228
|
return { problem: error instanceof Error ? error.message : String(error) };
|
|
3179
3229
|
}
|
|
3180
3230
|
}
|
|
3181
|
-
function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance) {
|
|
3231
|
+
function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance, configured) {
|
|
3182
3232
|
return {
|
|
3183
3233
|
diagnosisCode,
|
|
3184
3234
|
mcpRootsSupported: snapshot.supported,
|
|
3185
3235
|
processCwd,
|
|
3186
3236
|
rootCandidates,
|
|
3237
|
+
...configured ? { configuredProjectRoot: configured } : {},
|
|
3187
3238
|
guidance,
|
|
3188
3239
|
reportRecommended: false
|
|
3189
3240
|
};
|
|
3190
3241
|
}
|
|
3191
|
-
function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = []) {
|
|
3242
|
+
function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = [], configured) {
|
|
3192
3243
|
return {
|
|
3193
3244
|
diagnosisCode: "project_bound",
|
|
3194
3245
|
mcpRootsSupported: snapshot.supported,
|
|
3195
3246
|
processCwd,
|
|
3196
3247
|
rootCandidates,
|
|
3248
|
+
...configured ? { configuredProjectRoot: configured } : {},
|
|
3197
3249
|
selectedProjectDir: selected.projectDir,
|
|
3198
3250
|
bindingSource: selected.bindingSource,
|
|
3199
3251
|
guidance: `Sakupa is locked to ${selected.projectDir} from ${selected.bindingSource}.`,
|
|
@@ -3221,7 +3273,7 @@ var TARGET_MCP_TOOL_NAMES = [
|
|
|
3221
3273
|
"support",
|
|
3222
3274
|
"report"
|
|
3223
3275
|
];
|
|
3224
|
-
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
3276
|
+
var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
|
|
3225
3277
|
schemaVersion: z.literal(1),
|
|
3226
3278
|
outcome: z.enum([
|
|
3227
3279
|
"completed",
|
|
@@ -3303,7 +3355,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
|
3303
3355
|
reasonCode: z.string().optional()
|
|
3304
3356
|
})
|
|
3305
3357
|
)
|
|
3306
|
-
};
|
|
3358
|
+
});
|
|
3307
3359
|
function structuredToolResult(envelope) {
|
|
3308
3360
|
const clientTimeZone = clientRuntimeTimeZone();
|
|
3309
3361
|
const presentation = {
|
|
@@ -3412,14 +3464,19 @@ function resolverFor(ctx) {
|
|
|
3412
3464
|
if (ctx.projectBinding) return ctx.projectBinding;
|
|
3413
3465
|
let resolver = fallbackResolvers.get(ctx);
|
|
3414
3466
|
if (!resolver) {
|
|
3415
|
-
resolver = new ProjectBindingResolver(
|
|
3467
|
+
resolver = new ProjectBindingResolver(
|
|
3468
|
+
ctx.projectDir,
|
|
3469
|
+
ctx.rootsProvider,
|
|
3470
|
+
MCP_ROOTS_TIMEOUT_MS,
|
|
3471
|
+
ctx.configuredProjectRoot
|
|
3472
|
+
);
|
|
3416
3473
|
fallbackResolvers.set(ctx, resolver);
|
|
3417
3474
|
}
|
|
3418
3475
|
return resolver;
|
|
3419
3476
|
}
|
|
3420
|
-
async function withProjectDir(ctx) {
|
|
3477
|
+
async function withProjectDir(ctx, call) {
|
|
3421
3478
|
try {
|
|
3422
|
-
const binding = await resolverFor(ctx).resolve();
|
|
3479
|
+
const binding = await resolverFor(ctx).resolve(call);
|
|
3423
3480
|
const resolved = resolveLockedProjectRoot(binding.projectDir);
|
|
3424
3481
|
return {
|
|
3425
3482
|
...ctx,
|
|
@@ -3442,12 +3499,12 @@ async function withProjectDir(ctx) {
|
|
|
3442
3499
|
throw error;
|
|
3443
3500
|
}
|
|
3444
3501
|
}
|
|
3445
|
-
async function diagnoseProjectBinding(ctx) {
|
|
3446
|
-
return resolverFor(ctx).diagnose();
|
|
3502
|
+
async function diagnoseProjectBinding(ctx, call) {
|
|
3503
|
+
return resolverFor(ctx).diagnose(call);
|
|
3447
3504
|
}
|
|
3448
|
-
async function initializeWorkspaceProject(ctx) {
|
|
3505
|
+
async function initializeWorkspaceProject(ctx, call) {
|
|
3449
3506
|
try {
|
|
3450
|
-
const resolved = await resolverFor(ctx).initialize();
|
|
3507
|
+
const resolved = await resolverFor(ctx).initialize(call);
|
|
3451
3508
|
return {
|
|
3452
3509
|
...ctx,
|
|
3453
3510
|
projectDir: resolved.projectDir,
|
|
@@ -3463,10 +3520,11 @@ async function initializeWorkspaceProject(ctx) {
|
|
|
3463
3520
|
throw error;
|
|
3464
3521
|
}
|
|
3465
3522
|
}
|
|
3466
|
-
async function optionalProjectContext(ctx) {
|
|
3523
|
+
async function optionalProjectContext(ctx, call) {
|
|
3467
3524
|
try {
|
|
3468
|
-
return await withProjectDir(ctx);
|
|
3469
|
-
} catch {
|
|
3525
|
+
return await withProjectDir(ctx, call);
|
|
3526
|
+
} catch (error) {
|
|
3527
|
+
if (error instanceof McpRootsPending) throw error;
|
|
3470
3528
|
return null;
|
|
3471
3529
|
}
|
|
3472
3530
|
}
|
|
@@ -3514,6 +3572,9 @@ function requireSiteFile(ctx) {
|
|
|
3514
3572
|
}
|
|
3515
3573
|
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
3574
|
function toolError(e) {
|
|
3575
|
+
if (e instanceof McpRootsPending) {
|
|
3576
|
+
return inputRequired({ inputRequests: { roots: inputRequired.listRoots() } });
|
|
3577
|
+
}
|
|
3517
3578
|
const isSakupa = isSakupaError(e);
|
|
3518
3579
|
const errorCode = isSakupa ? e.code : "internal";
|
|
3519
3580
|
const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
|
|
@@ -4030,13 +4091,13 @@ function registerTools(server, baseCtx) {
|
|
|
4030
4091
|
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
4092
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4032
4093
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
4033
|
-
inputSchema: {
|
|
4094
|
+
inputSchema: z2.object({
|
|
4034
4095
|
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
4035
|
-
}
|
|
4096
|
+
})
|
|
4036
4097
|
},
|
|
4037
|
-
async (args) => {
|
|
4098
|
+
async (args, call) => {
|
|
4038
4099
|
try {
|
|
4039
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4100
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4040
4101
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
4041
4102
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
4042
4103
|
});
|
|
@@ -4057,7 +4118,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4057
4118
|
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
4119
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4059
4120
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4060
|
-
inputSchema: {
|
|
4121
|
+
inputSchema: z2.object({
|
|
4061
4122
|
outputDir: z2.string().min(1).describe(
|
|
4062
4123
|
'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
4124
|
),
|
|
@@ -4083,12 +4144,12 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4083
4144
|
"Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
|
|
4084
4145
|
),
|
|
4085
4146
|
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
4086
|
-
}
|
|
4147
|
+
})
|
|
4087
4148
|
},
|
|
4088
|
-
async (args) => {
|
|
4149
|
+
async (args, call) => {
|
|
4089
4150
|
let releaseHandoffLock;
|
|
4090
4151
|
try {
|
|
4091
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4152
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4092
4153
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
4093
4154
|
if (!analysis.deployable || !analysis.files) {
|
|
4094
4155
|
return notDeployableResult(analysis);
|
|
@@ -4713,11 +4774,11 @@ Optional security recommendation: this management credential was created at ${ti
|
|
|
4713
4774
|
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
4775
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4715
4776
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4716
|
-
inputSchema: {}
|
|
4777
|
+
inputSchema: z2.object({})
|
|
4717
4778
|
},
|
|
4718
|
-
async () => {
|
|
4779
|
+
async (_args, call) => {
|
|
4719
4780
|
try {
|
|
4720
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4781
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4721
4782
|
const site = requireSiteFile(ctx);
|
|
4722
4783
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
4723
4784
|
if (site.url) {
|
|
@@ -4746,11 +4807,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4746
4807
|
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
4808
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4748
4809
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
4749
|
-
inputSchema: {}
|
|
4810
|
+
inputSchema: z2.object({})
|
|
4750
4811
|
},
|
|
4751
|
-
async () => {
|
|
4812
|
+
async (_args, call) => {
|
|
4752
4813
|
try {
|
|
4753
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4814
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4754
4815
|
const site = requireSiteFile(ctx);
|
|
4755
4816
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
4756
4817
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -4777,15 +4838,15 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4777
4838
|
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
4839
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4779
4840
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4780
|
-
inputSchema: {
|
|
4841
|
+
inputSchema: z2.object({
|
|
4781
4842
|
plan: planEnum.describe(
|
|
4782
4843
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
4783
4844
|
)
|
|
4784
|
-
}
|
|
4845
|
+
})
|
|
4785
4846
|
},
|
|
4786
|
-
async (args) => {
|
|
4847
|
+
async (args, call) => {
|
|
4787
4848
|
try {
|
|
4788
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4849
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4789
4850
|
const site = requireSiteFile(ctx);
|
|
4790
4851
|
const res = await ctx.client.createPlanCheckout(
|
|
4791
4852
|
{
|
|
@@ -4823,17 +4884,17 @@ Once Stripe confirms payment and Sakupa synchronizes the subscription, the curre
|
|
|
4823
4884
|
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
4885
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4825
4886
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4826
|
-
inputSchema: {
|
|
4887
|
+
inputSchema: z2.object({
|
|
4827
4888
|
action: z2.enum(["start", "status"]),
|
|
4828
4889
|
hostname: z2.string().optional().describe("Required for start."),
|
|
4829
4890
|
verificationId: z2.string().optional().describe(
|
|
4830
4891
|
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
4831
4892
|
)
|
|
4832
|
-
}
|
|
4893
|
+
})
|
|
4833
4894
|
},
|
|
4834
|
-
async (args) => {
|
|
4895
|
+
async (args, call) => {
|
|
4835
4896
|
try {
|
|
4836
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4897
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4837
4898
|
const site = requireSiteFile(ctx);
|
|
4838
4899
|
if (args.action === "status") {
|
|
4839
4900
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -4962,11 +5023,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
4962
5023
|
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
5024
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4964
5025
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
4965
|
-
inputSchema: {}
|
|
5026
|
+
inputSchema: z2.object({})
|
|
4966
5027
|
},
|
|
4967
|
-
async () => {
|
|
5028
|
+
async (_args, call) => {
|
|
4968
5029
|
try {
|
|
4969
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5030
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4970
5031
|
const site = requireSiteFile(ctx);
|
|
4971
5032
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
4972
5033
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -5006,14 +5067,14 @@ Full status:`, res);
|
|
|
5006
5067
|
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
5068
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5008
5069
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5009
|
-
inputSchema: {
|
|
5070
|
+
inputSchema: z2.object({
|
|
5010
5071
|
scope: z2.enum(["site", "public_recovery"])
|
|
5011
|
-
}
|
|
5072
|
+
})
|
|
5012
5073
|
},
|
|
5013
|
-
async (args) => {
|
|
5074
|
+
async (args, call) => {
|
|
5014
5075
|
try {
|
|
5015
5076
|
if (args.scope === "site") {
|
|
5016
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5077
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5017
5078
|
const site = requireSiteFile(ctx);
|
|
5018
5079
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
5019
5080
|
return structuredToolResult({
|
|
@@ -5062,7 +5123,7 @@ Full status:`, res);
|
|
|
5062
5123
|
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
5124
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5064
5125
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
5065
|
-
inputSchema: {
|
|
5126
|
+
inputSchema: z2.object({
|
|
5066
5127
|
action: z2.enum(["start", "status", "complete", "download"]),
|
|
5067
5128
|
hostname: z2.string().optional().describe("Required for start."),
|
|
5068
5129
|
verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
@@ -5070,11 +5131,11 @@ Full status:`, res);
|
|
|
5070
5131
|
"REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
|
|
5071
5132
|
),
|
|
5072
5133
|
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
5073
|
-
}
|
|
5134
|
+
})
|
|
5074
5135
|
},
|
|
5075
|
-
async (args) => {
|
|
5136
|
+
async (args, call) => {
|
|
5076
5137
|
try {
|
|
5077
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5138
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5078
5139
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
5079
5140
|
throw new LocalGuidanceError(
|
|
5080
5141
|
"invalid_request",
|
|
@@ -5423,16 +5484,16 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5423
5484
|
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
5485
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5425
5486
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5426
|
-
inputSchema: {
|
|
5487
|
+
inputSchema: z2.object({
|
|
5427
5488
|
category: ticketCategoryEnum,
|
|
5428
5489
|
subject: z2.string().describe("Short subject line."),
|
|
5429
5490
|
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
5430
5491
|
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
5431
|
-
}
|
|
5492
|
+
})
|
|
5432
5493
|
},
|
|
5433
|
-
async (args) => {
|
|
5494
|
+
async (args, call) => {
|
|
5434
5495
|
try {
|
|
5435
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5496
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5436
5497
|
const site = requireSiteFile(ctx);
|
|
5437
5498
|
const res = await ctx.client.createTicket(site.credential, {
|
|
5438
5499
|
siteId: site.siteId,
|
|
@@ -5457,7 +5518,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5457
5518
|
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
5519
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5459
5520
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5460
|
-
inputSchema: {
|
|
5521
|
+
inputSchema: z2.object({
|
|
5461
5522
|
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
5462
5523
|
helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
|
|
5463
5524
|
errorCode: z2.string().optional(),
|
|
@@ -5473,12 +5534,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5473
5534
|
"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
5535
|
),
|
|
5475
5536
|
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
5476
|
-
}
|
|
5537
|
+
})
|
|
5477
5538
|
},
|
|
5478
|
-
async (args) => {
|
|
5539
|
+
async (args, call) => {
|
|
5479
5540
|
try {
|
|
5480
5541
|
requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
|
|
5481
|
-
const ctx = await optionalProjectContext(baseCtx);
|
|
5542
|
+
const ctx = await optionalProjectContext(baseCtx, call);
|
|
5482
5543
|
const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
|
|
5483
5544
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
5484
5545
|
const diagnostics = {
|
|
@@ -5558,11 +5619,11 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5558
5619
|
"plans",
|
|
5559
5620
|
{
|
|
5560
5621
|
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: {},
|
|
5622
|
+
inputSchema: z3.object({}),
|
|
5562
5623
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5563
5624
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
5564
5625
|
},
|
|
5565
|
-
async () => {
|
|
5626
|
+
async (_args, call) => {
|
|
5566
5627
|
try {
|
|
5567
5628
|
const catalog = await baseCtx.client.getBillingPlanCatalog();
|
|
5568
5629
|
return structuredToolResult({
|
|
@@ -5582,15 +5643,15 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5582
5643
|
"change",
|
|
5583
5644
|
{
|
|
5584
5645
|
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: {
|
|
5646
|
+
inputSchema: z3.object({
|
|
5586
5647
|
operationId: z3.string().min(1)
|
|
5587
|
-
},
|
|
5648
|
+
}),
|
|
5588
5649
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5589
5650
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
5590
5651
|
},
|
|
5591
|
-
async (args) => {
|
|
5652
|
+
async (args, call) => {
|
|
5592
5653
|
try {
|
|
5593
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5654
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5594
5655
|
const site = requireSiteFile(ctx);
|
|
5595
5656
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
5596
5657
|
siteId: site.siteId,
|
|
@@ -5674,7 +5735,7 @@ var TOOL_MANUALS = {
|
|
|
5674
5735
|
init: {
|
|
5675
5736
|
purpose: "Initialize the active IDE workspace as one Sakupa project.",
|
|
5676
5737
|
sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
|
|
5677
|
-
preconditions: "Exactly one usable MCP workspace Root. If
|
|
5738
|
+
preconditions: "Exactly one usable MCP workspace Root, or the SAKUPA_PROJECT_ROOT directory configured for this server when the client provides no Roots. If neither exists, help may authorize the AI to use CLI init.",
|
|
5678
5739
|
parameterNames: [],
|
|
5679
5740
|
parameters: "No parameters and no path argument.",
|
|
5680
5741
|
warnings: [
|
|
@@ -5873,13 +5934,13 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5873
5934
|
"init",
|
|
5874
5935
|
{
|
|
5875
5936
|
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: {},
|
|
5937
|
+
inputSchema: z4.object({}),
|
|
5877
5938
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5878
5939
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
5879
5940
|
},
|
|
5880
|
-
async () => {
|
|
5941
|
+
async (_args, call) => {
|
|
5881
5942
|
try {
|
|
5882
|
-
const ctx = await initializeWorkspaceProject(baseCtx);
|
|
5943
|
+
const ctx = await initializeWorkspaceProject(baseCtx, call);
|
|
5883
5944
|
const marker = loadProjectMarker(ctx.projectDir);
|
|
5884
5945
|
if (marker.kind !== "ok")
|
|
5885
5946
|
throw new Error("init postcondition failed: project marker missing");
|
|
@@ -5913,17 +5974,17 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5913
5974
|
"help",
|
|
5914
5975
|
{
|
|
5915
5976
|
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: {
|
|
5977
|
+
inputSchema: z4.object({
|
|
5917
5978
|
topic: z4.enum(HELP_TOPICS).optional().default("diagnose"),
|
|
5918
5979
|
failedTool: z4.string().optional(),
|
|
5919
5980
|
errorCode: z4.string().optional(),
|
|
5920
5981
|
resultCode: z4.string().optional(),
|
|
5921
5982
|
requestId: z4.string().optional()
|
|
5922
|
-
},
|
|
5983
|
+
}),
|
|
5923
5984
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5924
5985
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
5925
5986
|
},
|
|
5926
|
-
async (args) => {
|
|
5987
|
+
async (args, call) => {
|
|
5927
5988
|
try {
|
|
5928
5989
|
if (args.topic === "overview") {
|
|
5929
5990
|
const catalog = Object.fromEntries(
|
|
@@ -5985,7 +6046,7 @@ Terminology: ${terminologyText}` : ""),
|
|
|
5985
6046
|
nextActions: []
|
|
5986
6047
|
});
|
|
5987
6048
|
}
|
|
5988
|
-
const diagnosis = await diagnoseProjectBinding(baseCtx);
|
|
6049
|
+
const diagnosis = await diagnoseProjectBinding(baseCtx, call);
|
|
5989
6050
|
const selected = diagnosis.selectedProjectDir;
|
|
5990
6051
|
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
5991
6052
|
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
@@ -6064,18 +6125,18 @@ function registerCredentialTools(server, baseCtx) {
|
|
|
6064
6125
|
"rotate",
|
|
6065
6126
|
{
|
|
6066
6127
|
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: {
|
|
6128
|
+
inputSchema: z5.object({
|
|
6068
6129
|
confirmed: z5.boolean().optional().describe(
|
|
6069
6130
|
"True only after showing the rotate preview and the user explicitly approves revoking every old credential."
|
|
6070
6131
|
)
|
|
6071
|
-
},
|
|
6132
|
+
}),
|
|
6072
6133
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
6073
6134
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
6074
6135
|
},
|
|
6075
|
-
async (args) => {
|
|
6136
|
+
async (args, call) => {
|
|
6076
6137
|
let releaseLock;
|
|
6077
6138
|
try {
|
|
6078
|
-
const ctx = await withProjectDir(baseCtx);
|
|
6139
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
6079
6140
|
let site = requireSiteFile(ctx);
|
|
6080
6141
|
const pending = loadCredentialRotation(ctx.projectDir);
|
|
6081
6142
|
if (pending.kind !== "absent" || args.confirmed === true) {
|
|
@@ -6382,11 +6443,13 @@ language. Keep option IDs, tool names, exact arguments, URLs, field names and co
|
|
|
6382
6443
|
unchanged.
|
|
6383
6444
|
|
|
6384
6445
|
Project directory contract: before the first deploy or a new recovery, CALL the init MCP tool with
|
|
6385
|
-
NO path argument. init uses the IDE's exact MCP Root
|
|
6446
|
+
NO path argument. init uses the IDE's exact MCP Root \u2014 or, when the client provides no Roots, the
|
|
6447
|
+
SAKUPA_PROJECT_ROOT directory configured for this MCP server \u2014 and creates the non-secret
|
|
6386
6448
|
.sakupa/project.json directly there. Do not merely print installation or CLI instructions when the
|
|
6387
|
-
init tool is available. Only after help confirms that the client
|
|
6388
|
-
AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a
|
|
6389
|
-
run it. The CLI also accepts NO path argument. ONE MCP process = ONE
|
|
6449
|
+
init tool is available. Only after help confirms that the client provides neither MCP Roots nor
|
|
6450
|
+
SAKUPA_PROJECT_ROOT may the AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a
|
|
6451
|
+
fallback; never ask the user to run it. The CLI also accepts NO path argument. ONE MCP process = ONE
|
|
6452
|
+
locked project = ONE site.
|
|
6390
6453
|
Site tools do not accept projectDir and cannot select another root; help, plans and report preview
|
|
6391
6454
|
and public_recovery portal remain project-independent.
|
|
6392
6455
|
Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
|
|
@@ -6466,29 +6529,19 @@ function createSakupaMcpServer(opts) {
|
|
|
6466
6529
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
6467
6530
|
);
|
|
6468
6531
|
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
|
-
});
|
|
6532
|
+
const rootsProvider = opts.rootsProvider ?? ((call) => readClientRoots(server, call));
|
|
6486
6533
|
const ctx = {
|
|
6487
6534
|
client,
|
|
6488
6535
|
apiBaseUrl: opts.apiBaseUrl,
|
|
6489
6536
|
projectDir: processCwd,
|
|
6490
6537
|
rootsProvider,
|
|
6491
|
-
|
|
6538
|
+
...opts.projectRoot !== void 0 ? { configuredProjectRoot: opts.projectRoot } : {},
|
|
6539
|
+
projectBinding: new ProjectBindingResolver(
|
|
6540
|
+
processCwd,
|
|
6541
|
+
rootsProvider,
|
|
6542
|
+
MCP_ROOTS_TIMEOUT_MS,
|
|
6543
|
+
opts.projectRoot
|
|
6544
|
+
)
|
|
6492
6545
|
};
|
|
6493
6546
|
registerTools(server, ctx);
|
|
6494
6547
|
registerBillingTools(server, ctx);
|
|
@@ -6496,6 +6549,38 @@ function createSakupaMcpServer(opts) {
|
|
|
6496
6549
|
registerHelpTools(server, ctx);
|
|
6497
6550
|
return server;
|
|
6498
6551
|
}
|
|
6552
|
+
async function readClientRoots(server, call) {
|
|
6553
|
+
if (call?.mcpReq.envelope !== void 0) {
|
|
6554
|
+
const envelope = call.mcpReq.envelope;
|
|
6555
|
+
const declared = envelope[CLIENT_CAPABILITIES_META_KEY];
|
|
6556
|
+
if (!declared?.roots) return { supported: false, roots: [] };
|
|
6557
|
+
const answered = inputResponse(call.mcpReq.inputResponses, "roots");
|
|
6558
|
+
if (answered.kind === "roots") return { supported: true, roots: answered.roots };
|
|
6559
|
+
if (call.mcpReq.inputResponses !== void 0) {
|
|
6560
|
+
return {
|
|
6561
|
+
supported: true,
|
|
6562
|
+
roots: [],
|
|
6563
|
+
error: "The client retried without answering the embedded roots/list request."
|
|
6564
|
+
};
|
|
6565
|
+
}
|
|
6566
|
+
throw new McpRootsPending();
|
|
6567
|
+
}
|
|
6568
|
+
const capabilities = server.server.getClientCapabilities();
|
|
6569
|
+
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
6570
|
+
try {
|
|
6571
|
+
const response = await server.server.listRoots(void 0, {
|
|
6572
|
+
timeout: MCP_ROOTS_TIMEOUT_MS,
|
|
6573
|
+
maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
|
|
6574
|
+
});
|
|
6575
|
+
return { supported: true, roots: response.roots };
|
|
6576
|
+
} catch (error) {
|
|
6577
|
+
return {
|
|
6578
|
+
supported: true,
|
|
6579
|
+
roots: [],
|
|
6580
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6581
|
+
};
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6499
6584
|
|
|
6500
6585
|
// src/bin.ts
|
|
6501
6586
|
async function main() {
|
|
@@ -6508,15 +6593,20 @@ async function main() {
|
|
|
6508
6593
|
process.exitCode = result.exitCode;
|
|
6509
6594
|
return;
|
|
6510
6595
|
}
|
|
6596
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
6597
|
+
stdout.write(`${MCP_VERSION}
|
|
6598
|
+
`);
|
|
6599
|
+
return;
|
|
6600
|
+
}
|
|
6511
6601
|
if (argv.length > 0) {
|
|
6512
|
-
throw new Error("Usage: sakupa-mcp [init]");
|
|
6602
|
+
throw new Error("Usage: sakupa-mcp [init | --version]");
|
|
6513
6603
|
}
|
|
6514
6604
|
const config = loadMcpRuntimeConfig();
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6605
|
+
serveStdio(() => createSakupaMcpServer(config), {
|
|
6606
|
+
onerror: (error) => console.error("[sakupa-mcp] transport error:", error.message)
|
|
6607
|
+
});
|
|
6518
6608
|
console.error(
|
|
6519
|
-
`[sakupa-mcp] v${MCP_VERSION}
|
|
6609
|
+
`[sakupa-mcp] v${MCP_VERSION} serving stdio (api: ${config.apiBaseUrl}; configured root: ${config.projectRoot ?? "none"}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
|
|
6520
6610
|
);
|
|
6521
6611
|
}
|
|
6522
6612
|
main().catch((err2) => {
|