webcake-storefront-mcp 1.0.1
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/README.md +1166 -0
- package/dist/api.js +346 -0
- package/dist/auth/login.js +87 -0
- package/dist/builder/catalog.js +186 -0
- package/dist/builder/factory.js +1677 -0
- package/dist/builder/guide.js +64 -0
- package/dist/builder/page.js +149 -0
- package/dist/config.js +97 -0
- package/dist/db.js +96 -0
- package/dist/guides.js +93 -0
- package/dist/http.js +120 -0
- package/dist/index.js +73 -0
- package/dist/install.js +140 -0
- package/dist/mongo.js +102 -0
- package/dist/server.js +63 -0
- package/dist/smoke.js +81 -0
- package/dist/tools/apps.js +7 -0
- package/dist/tools/articles.js +53 -0
- package/dist/tools/automation.js +8 -0
- package/dist/tools/builder-extras.js +165 -0
- package/dist/tools/builder.js +124 -0
- package/dist/tools/cms-files.js +255 -0
- package/dist/tools/collections.js +31 -0
- package/dist/tools/combos.js +72 -0
- package/dist/tools/context.js +158 -0
- package/dist/tools/customers.js +13 -0
- package/dist/tools/global-sources.js +662 -0
- package/dist/tools/images.js +875 -0
- package/dist/tools/orders.js +32 -0
- package/dist/tools/pages.js +621 -0
- package/dist/tools/products.js +38 -0
- package/dist/tools/promotions.js +131 -0
- package/dist/tools/site-style.js +157 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
import { makeApi, resolveSettings } from "./config.js";
|
|
5
|
+
const HELP = `webcake-storefront-mcp — MCP server for the WebCake/StoreCake storefront builder
|
|
6
|
+
|
|
7
|
+
Usage: npx -y webcake-storefront-mcp [command] [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
(none) start the stdio MCP server (use this in IDE configs)
|
|
11
|
+
install configure the server in your IDE(s) — interactive or via flags
|
|
12
|
+
uninstall remove the server from your IDE configs
|
|
13
|
+
login grab your token via the browser (saved to the local config db)
|
|
14
|
+
serve [--port N] run the remote Streamable-HTTP server (default 8787; or PORT env)
|
|
15
|
+
help, --help, -h show this help
|
|
16
|
+
|
|
17
|
+
Global options:
|
|
18
|
+
--env <local|staging|prod> pick the API + app base URLs (default prod)
|
|
19
|
+
`;
|
|
20
|
+
/** Read a global `--env <name>` / `--env=<name>` flag and expose it via WEBCAKE_ENV
|
|
21
|
+
* so every subcommand (stdio, install, login, serve) resolves the same endpoints. */
|
|
22
|
+
function applyEnvFlag(argv) {
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === "--env" && argv[i + 1])
|
|
26
|
+
process.env.WEBCAKE_ENV = argv[i + 1];
|
|
27
|
+
else if (a.startsWith("--env="))
|
|
28
|
+
process.env.WEBCAKE_ENV = a.slice("--env=".length);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function main() {
|
|
32
|
+
applyEnvFlag(process.argv);
|
|
33
|
+
const sub = process.argv[2];
|
|
34
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
35
|
+
process.stdout.write(HELP);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (sub === "install" || sub === "uninstall") {
|
|
39
|
+
const { runInstaller } = await import("./install.js");
|
|
40
|
+
const rest = sub === "uninstall" ? ["--uninstall", ...process.argv.slice(3)] : process.argv.slice(3);
|
|
41
|
+
await runInstaller(rest);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (sub === "login") {
|
|
45
|
+
const { runLogin } = await import("./auth/login.js");
|
|
46
|
+
await runLogin(process.argv.slice(3));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (sub === "serve" || sub === "http" || sub === "serve-http") {
|
|
50
|
+
const { startHttpServer } = await import("./http.js");
|
|
51
|
+
const flagIdx = process.argv.indexOf("--port");
|
|
52
|
+
const raw = (flagIdx !== -1 ? process.argv[flagIdx + 1] : undefined) ?? process.env.PORT;
|
|
53
|
+
const port = Number(raw);
|
|
54
|
+
await startHttpServer(Number.isFinite(port) && port > 0 ? port : 8787);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Default: stdio MCP server.
|
|
58
|
+
const settings = resolveSettings();
|
|
59
|
+
if (!settings.apiUrl) {
|
|
60
|
+
console.error("Required: WEBCAKE_API_URL (env var or saved via `login` / update_auth tool).");
|
|
61
|
+
console.error("Run `npx -y webcake-storefront-mcp install` to set things up.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const api = makeApi();
|
|
65
|
+
const server = createServer(api);
|
|
66
|
+
const transport = new StdioServerTransport();
|
|
67
|
+
await server.connect(transport);
|
|
68
|
+
console.error("[webcake-storefront] MCP server ready on stdio.");
|
|
69
|
+
}
|
|
70
|
+
main().catch((err) => {
|
|
71
|
+
console.error("[webcake-storefront] fatal:", err);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Installer: writes (or removes) this MCP server's entry in the config files of the
|
|
2
|
+
// supported IDEs. Flag-driven; falls back to a couple of interactive prompts on a TTY.
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
const SERVER_KEY = "webcake-storefront";
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const o = { uninstall: false, ides: [] };
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const a = argv[i];
|
|
13
|
+
if (a === "--uninstall")
|
|
14
|
+
o.uninstall = true;
|
|
15
|
+
else if (a === "--ide")
|
|
16
|
+
o.ides.push(...(argv[++i] || "").split(",").map((s) => s.trim()).filter(Boolean));
|
|
17
|
+
else if (a === "--env")
|
|
18
|
+
o.env = argv[++i];
|
|
19
|
+
else if (a === "--token" || a === "--jwt")
|
|
20
|
+
o.token = argv[++i];
|
|
21
|
+
else if (a === "--session" || a === "--session-id" || a === "--wsid")
|
|
22
|
+
o.sessionId = argv[++i];
|
|
23
|
+
else if (a === "--site" || a === "--site-id")
|
|
24
|
+
o.siteId = argv[++i];
|
|
25
|
+
else if (a === "--api-url" || a === "--api-base")
|
|
26
|
+
o.apiUrl = argv[++i];
|
|
27
|
+
else if (a === "--npx")
|
|
28
|
+
o.launch = "npx";
|
|
29
|
+
else if (a === "--local")
|
|
30
|
+
o.launch = "local";
|
|
31
|
+
}
|
|
32
|
+
return o;
|
|
33
|
+
}
|
|
34
|
+
const HOME = homedir();
|
|
35
|
+
const APP_SUPPORT = process.platform === "darwin" ? join(HOME, "Library", "Application Support") : join(HOME, ".config");
|
|
36
|
+
// IDE -> config file + whether it nests under "mcpServers" (vs "mcp").
|
|
37
|
+
const IDE_CONFIGS = {
|
|
38
|
+
"claude-desktop": { path: join(APP_SUPPORT, "Claude", "claude_desktop_config.json"), key: "mcpServers" },
|
|
39
|
+
"claude-code": { path: join(HOME, ".claude.json"), key: "mcpServers" },
|
|
40
|
+
cursor: { path: join(HOME, ".cursor", "mcp.json"), key: "mcpServers" },
|
|
41
|
+
windsurf: { path: join(HOME, ".codeium", "windsurf", "mcp_config.json"), key: "mcpServers" },
|
|
42
|
+
vscode: { path: join(APP_SUPPORT, "Code", "User", "mcp.json"), key: "mcpServers" },
|
|
43
|
+
};
|
|
44
|
+
const ALL_IDES = Object.keys(IDE_CONFIGS);
|
|
45
|
+
function resolveLaunch(opts) {
|
|
46
|
+
const self = fileURLToPath(import.meta.url);
|
|
47
|
+
const ranViaNpx = self.includes("/_npx/") || self.includes("\\_npx\\");
|
|
48
|
+
const useNpx = opts.launch === "npx" || (opts.launch !== "local" && ranViaNpx);
|
|
49
|
+
if (useNpx)
|
|
50
|
+
return { command: "npx", args: ["-y", "webcake-storefront-mcp"] };
|
|
51
|
+
const entry = join(dirname(self), "index.js");
|
|
52
|
+
return { command: "node", args: [entry] };
|
|
53
|
+
}
|
|
54
|
+
function buildEnv(opts) {
|
|
55
|
+
const env = {};
|
|
56
|
+
if (opts.env)
|
|
57
|
+
env.WEBCAKE_ENV = opts.env;
|
|
58
|
+
if (opts.apiUrl)
|
|
59
|
+
env.WEBCAKE_API_URL = opts.apiUrl;
|
|
60
|
+
if (opts.token)
|
|
61
|
+
env.WEBCAKE_TOKEN = opts.token;
|
|
62
|
+
if (opts.sessionId)
|
|
63
|
+
env.WEBCAKE_SESSION_ID = opts.sessionId;
|
|
64
|
+
if (opts.siteId)
|
|
65
|
+
env.WEBCAKE_SITE_ID = opts.siteId;
|
|
66
|
+
return env;
|
|
67
|
+
}
|
|
68
|
+
function readJson(path) {
|
|
69
|
+
if (!existsSync(path))
|
|
70
|
+
return {};
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeJson(path, data) {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
81
|
+
}
|
|
82
|
+
function applyToIde(ide, opts, launch, env) {
|
|
83
|
+
const cfg = IDE_CONFIGS[ide];
|
|
84
|
+
if (!cfg)
|
|
85
|
+
return `skip ${ide} (unknown)`;
|
|
86
|
+
const json = readJson(cfg.path);
|
|
87
|
+
const bag = (json[cfg.key] ||= {});
|
|
88
|
+
if (opts.uninstall) {
|
|
89
|
+
if (!(SERVER_KEY in bag))
|
|
90
|
+
return `${ide}: nothing to remove`;
|
|
91
|
+
delete bag[SERVER_KEY];
|
|
92
|
+
writeJson(cfg.path, json);
|
|
93
|
+
return `${ide}: removed (${cfg.path})`;
|
|
94
|
+
}
|
|
95
|
+
bag[SERVER_KEY] = {
|
|
96
|
+
command: launch.command,
|
|
97
|
+
args: launch.args,
|
|
98
|
+
...(Object.keys(env).length ? { env } : {}),
|
|
99
|
+
};
|
|
100
|
+
writeJson(cfg.path, json);
|
|
101
|
+
return `${ide}: configured (${cfg.path})`;
|
|
102
|
+
}
|
|
103
|
+
async function promptMissing(opts) {
|
|
104
|
+
if (!process.stdin.isTTY)
|
|
105
|
+
return;
|
|
106
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
107
|
+
try {
|
|
108
|
+
if (!opts.ides.length) {
|
|
109
|
+
const ans = (await rl.question(`IDE(s) [${ALL_IDES.join(", ")}, all]: `)).trim();
|
|
110
|
+
opts.ides = ans === "all" || ans === "" ? ALL_IDES : ans.split(",").map((s) => s.trim());
|
|
111
|
+
}
|
|
112
|
+
if (!opts.uninstall && !opts.token && !process.env.WEBCAKE_TOKEN) {
|
|
113
|
+
const t = (await rl.question("Token (paste JWT, or leave blank to set later): ")).trim();
|
|
114
|
+
if (t)
|
|
115
|
+
opts.token = t;
|
|
116
|
+
}
|
|
117
|
+
if (!opts.uninstall && !opts.siteId && !process.env.WEBCAKE_SITE_ID) {
|
|
118
|
+
const s = (await rl.question("Site ID (optional): ")).trim();
|
|
119
|
+
if (s)
|
|
120
|
+
opts.siteId = s;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
rl.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export async function runInstaller(argv) {
|
|
128
|
+
const opts = parseArgs(argv);
|
|
129
|
+
await promptMissing(opts);
|
|
130
|
+
let ides = opts.ides.length ? opts.ides : ALL_IDES;
|
|
131
|
+
if (ides.includes("all"))
|
|
132
|
+
ides = ALL_IDES;
|
|
133
|
+
const launch = resolveLaunch(opts);
|
|
134
|
+
const env = buildEnv(opts);
|
|
135
|
+
console.error(opts.uninstall ? "Removing webcake-storefront MCP…" : "Configuring webcake-storefront MCP…");
|
|
136
|
+
for (const ide of ides)
|
|
137
|
+
console.error(" " + applyToIde(ide, opts, launch, env));
|
|
138
|
+
if (!opts.uninstall)
|
|
139
|
+
console.error("\nDone. Restart your IDE to pick up the new MCP server.");
|
|
140
|
+
}
|
package/dist/mongo.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Optional MongoDB sync layer for the image alt cache.
|
|
2
|
+
// Activates when MONGO_URI env var is set. Silently no-op when absent.
|
|
3
|
+
const MONGO_URI = process.env.MONGO_URI || "";
|
|
4
|
+
const MONGO_DB = process.env.MONGO_DB || "webcake_mcp";
|
|
5
|
+
const MONGO_COLLECTION = process.env.MONGO_COLLECTION || "image_alt_cache";
|
|
6
|
+
let _client = null;
|
|
7
|
+
let _collection = null;
|
|
8
|
+
let _connecting = null;
|
|
9
|
+
async function connect() {
|
|
10
|
+
if (!MONGO_URI)
|
|
11
|
+
return null;
|
|
12
|
+
if (_collection)
|
|
13
|
+
return _collection;
|
|
14
|
+
if (_connecting)
|
|
15
|
+
return _connecting;
|
|
16
|
+
_connecting = (async () => {
|
|
17
|
+
try {
|
|
18
|
+
const { MongoClient } = await import("mongodb");
|
|
19
|
+
_client = new MongoClient(MONGO_URI, { serverSelectionTimeoutMS: 5000 });
|
|
20
|
+
await _client.connect();
|
|
21
|
+
const db = _client.db(MONGO_DB);
|
|
22
|
+
_collection = db.collection(MONGO_COLLECTION);
|
|
23
|
+
await _collection.createIndex({ url_key: 1 }, { unique: true });
|
|
24
|
+
return _collection;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
_connecting = null;
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
return _connecting;
|
|
32
|
+
}
|
|
33
|
+
export function isMongoEnabled() {
|
|
34
|
+
return !!MONGO_URI;
|
|
35
|
+
}
|
|
36
|
+
export async function mongoUpsertAlts(items) {
|
|
37
|
+
if (!isMongoEnabled())
|
|
38
|
+
return { ok: false, reason: "MONGO_URI not set" };
|
|
39
|
+
const col = await connect();
|
|
40
|
+
if (!col)
|
|
41
|
+
return { ok: false, reason: "no collection" };
|
|
42
|
+
if (!items.length)
|
|
43
|
+
return { ok: true, upserted: 0 };
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const ops = items.map((it) => ({
|
|
46
|
+
updateOne: {
|
|
47
|
+
filter: { url_key: it.url_key },
|
|
48
|
+
update: {
|
|
49
|
+
$set: {
|
|
50
|
+
url_key: it.url_key,
|
|
51
|
+
url: it.url,
|
|
52
|
+
alt: it.alt,
|
|
53
|
+
source: it.source || "ai",
|
|
54
|
+
updated_at: now,
|
|
55
|
+
},
|
|
56
|
+
$setOnInsert: { created_at: now },
|
|
57
|
+
},
|
|
58
|
+
upsert: true,
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
const res = await col.bulkWrite(ops, { ordered: false });
|
|
62
|
+
return { ok: true, upserted: res.upsertedCount, modified: res.modifiedCount };
|
|
63
|
+
}
|
|
64
|
+
export async function mongoFindAlts(urlKeys) {
|
|
65
|
+
if (!isMongoEnabled() || !urlKeys.length)
|
|
66
|
+
return new Map();
|
|
67
|
+
const col = await connect();
|
|
68
|
+
if (!col)
|
|
69
|
+
return new Map();
|
|
70
|
+
const cursor = col.find({ url_key: { $in: urlKeys } });
|
|
71
|
+
const map = new Map();
|
|
72
|
+
for await (const doc of cursor) {
|
|
73
|
+
map.set(doc.url_key, doc);
|
|
74
|
+
}
|
|
75
|
+
return map;
|
|
76
|
+
}
|
|
77
|
+
export async function mongoListAlts(limit = 100, offset = 0) {
|
|
78
|
+
if (!isMongoEnabled())
|
|
79
|
+
return { total: 0, entries: [] };
|
|
80
|
+
const col = await connect();
|
|
81
|
+
if (!col)
|
|
82
|
+
return { total: 0, entries: [] };
|
|
83
|
+
const total = await col.countDocuments();
|
|
84
|
+
const entries = await col
|
|
85
|
+
.find({}, { projection: { _id: 0 } })
|
|
86
|
+
.sort({ updated_at: -1 })
|
|
87
|
+
.skip(offset)
|
|
88
|
+
.limit(limit)
|
|
89
|
+
.toArray();
|
|
90
|
+
return { total, entries };
|
|
91
|
+
}
|
|
92
|
+
export async function mongoCloseQuietly() {
|
|
93
|
+
if (_client) {
|
|
94
|
+
try {
|
|
95
|
+
await _client.close();
|
|
96
|
+
}
|
|
97
|
+
catch { /* ignore */ }
|
|
98
|
+
_client = null;
|
|
99
|
+
_collection = null;
|
|
100
|
+
_connecting = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerContextTools } from "./tools/context.js";
|
|
3
|
+
import { registerCmsFileTools } from "./tools/cms-files.js";
|
|
4
|
+
import { registerPageTools } from "./tools/pages.js";
|
|
5
|
+
import { registerCollectionTools } from "./tools/collections.js";
|
|
6
|
+
import { registerArticleTools } from "./tools/articles.js";
|
|
7
|
+
import { registerCustomerTools } from "./tools/customers.js";
|
|
8
|
+
import { registerAutomationTools } from "./tools/automation.js";
|
|
9
|
+
import { registerProductTools } from "./tools/products.js";
|
|
10
|
+
import { registerOrderTools } from "./tools/orders.js";
|
|
11
|
+
import { registerSiteStyleTools } from "./tools/site-style.js";
|
|
12
|
+
import { registerAppTools } from "./tools/apps.js";
|
|
13
|
+
import { registerPromotionTools } from "./tools/promotions.js";
|
|
14
|
+
import { registerComboTools } from "./tools/combos.js";
|
|
15
|
+
import { registerGlobalSourceTools } from "./tools/global-sources.js";
|
|
16
|
+
import { registerImageTools } from "./tools/images.js";
|
|
17
|
+
import { registerBuilderTools } from "./tools/builder.js";
|
|
18
|
+
import { registerBuilderExtraTools } from "./tools/builder-extras.js";
|
|
19
|
+
const INSTRUCTIONS = `You are an AI assistant connected to the WebCake/StoreCake storefront platform via MCP tools.
|
|
20
|
+
|
|
21
|
+
IMPORTANT: When the user asks ANY question about their website, store, products, orders, pages, or code — you MUST use the available tools to look up real data before answering. Never guess.
|
|
22
|
+
|
|
23
|
+
You can also BUILD pages: use get_build_guide, list_elements, get_element to learn the BuilderX component model, new_section/new_element to compose, validate_page to check, then build_page (dry_run first) to create. Publishing is site-level via publish_site.
|
|
24
|
+
|
|
25
|
+
Workflow:
|
|
26
|
+
1. On first interaction, call get_current_context to confirm the connected site.
|
|
27
|
+
2. Before answering a site-specific question, query the relevant tool.
|
|
28
|
+
3. When building a page, read get_build_guide first and validate before saving.
|
|
29
|
+
4. Always reply in the user's language; keep Vietnamese with full diacritics.`;
|
|
30
|
+
function makeResult(data) {
|
|
31
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
32
|
+
}
|
|
33
|
+
/** Build a fully-wired MCP server bound to the given API client. */
|
|
34
|
+
export function createServer(api) {
|
|
35
|
+
const server = new McpServer({ name: "webcake-storefront", version: "1.0.0" }, { instructions: INSTRUCTIONS });
|
|
36
|
+
const handle = async (fn) => {
|
|
37
|
+
try {
|
|
38
|
+
return makeResult(await fn());
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
42
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
registerContextTools(server, api, handle);
|
|
46
|
+
registerCmsFileTools(server, api, handle);
|
|
47
|
+
registerPageTools(server, api, handle);
|
|
48
|
+
registerCollectionTools(server, api, handle);
|
|
49
|
+
registerArticleTools(server, api, handle);
|
|
50
|
+
registerCustomerTools(server, api, handle);
|
|
51
|
+
registerAutomationTools(server, api, handle);
|
|
52
|
+
registerProductTools(server, api, handle);
|
|
53
|
+
registerOrderTools(server, api, handle);
|
|
54
|
+
registerSiteStyleTools(server, api, handle);
|
|
55
|
+
registerAppTools(server, api, handle);
|
|
56
|
+
registerPromotionTools(server, api, handle);
|
|
57
|
+
registerComboTools(server, api, handle);
|
|
58
|
+
registerGlobalSourceTools(server, api, handle);
|
|
59
|
+
registerImageTools(server, api, handle);
|
|
60
|
+
registerBuilderTools(server, api, handle);
|
|
61
|
+
registerBuilderExtraTools(server, api, handle);
|
|
62
|
+
return server;
|
|
63
|
+
}
|
package/dist/smoke.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline smoke test (no MCP transport, no DB/native deps): exercises the page-builder
|
|
3
|
+
* building blocks so a release can verify them without a client. Run: npm run smoke
|
|
4
|
+
*/
|
|
5
|
+
import { listElements, getElement, buildElement, ELEMENT_TYPES, isKnownType } from "./builder/catalog.js";
|
|
6
|
+
import { newPageSkeleton, buildSection, validatePage, reassignIds, walk, } from "./builder/page.js";
|
|
7
|
+
let failures = 0;
|
|
8
|
+
const check = (name, cond, extra) => {
|
|
9
|
+
if (cond) {
|
|
10
|
+
console.log(` ok ${name}`);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
failures++;
|
|
14
|
+
console.log(`FAIL ${name}`, extra ?? "");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
console.log("== catalog: factory registry is well-formed ==");
|
|
18
|
+
{
|
|
19
|
+
check("element types are non-empty", ELEMENT_TYPES.length > 0, ELEMENT_TYPES.length);
|
|
20
|
+
check("no duplicate element types", new Set(ELEMENT_TYPES).size === ELEMENT_TYPES.length);
|
|
21
|
+
const cat = listElements();
|
|
22
|
+
check("listElements total matches type count", cat.total === ELEMENT_TYPES.length);
|
|
23
|
+
check("listElements has categories", Object.keys(cat.categories).length > 0);
|
|
24
|
+
check("getElement('section') is a container", getElement("section").container === true);
|
|
25
|
+
check("isKnownType('button')", isKnownType("button"));
|
|
26
|
+
check("isKnownType('nope') is false", !isKnownType("nope"));
|
|
27
|
+
}
|
|
28
|
+
console.log("== factory: nodes are structurally valid ==");
|
|
29
|
+
{
|
|
30
|
+
const section = buildElement("section");
|
|
31
|
+
check("section has type + children[]", section.type === "section" && Array.isArray(section.children));
|
|
32
|
+
const text = buildElement("text", { text: "Hello" });
|
|
33
|
+
check("text carries specials.text", text.specials?.text === "Hello");
|
|
34
|
+
check("ids are prefixed by type", /^TEXT-/.test(text.id));
|
|
35
|
+
let threw = false;
|
|
36
|
+
try {
|
|
37
|
+
buildElement("definitely-not-a-type");
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
threw = e?.code === "UNKNOWN_TYPE";
|
|
41
|
+
}
|
|
42
|
+
check("buildElement throws UNKNOWN_TYPE for bad type", threw);
|
|
43
|
+
}
|
|
44
|
+
console.log("== page: grid composition + validation ==");
|
|
45
|
+
{
|
|
46
|
+
const skeleton = newPageSkeleton();
|
|
47
|
+
check("skeleton is { sections: [] }", Array.isArray(skeleton.sections) && skeleton.sections.length === 0);
|
|
48
|
+
const hero = buildSection([
|
|
49
|
+
{ type: "text", opts: { text: "Welcome" } },
|
|
50
|
+
{ type: "button", opts: { text: "Buy" } },
|
|
51
|
+
]);
|
|
52
|
+
check("section grid is 1xN", hero.runtime.config.grid === "1x2", hero.runtime.config.grid);
|
|
53
|
+
check("children get grid positions", hero.children.every((c) => c.runtime.config.columnStart === 1));
|
|
54
|
+
const src = newPageSkeleton();
|
|
55
|
+
src.sections.push(hero);
|
|
56
|
+
const v = validatePage(src);
|
|
57
|
+
check("built page validates", v.valid === true, v.errors);
|
|
58
|
+
check("stats count elements", v.stats.total_elements === 3, v.stats);
|
|
59
|
+
// duplicate ids must fail validation
|
|
60
|
+
const dup = newPageSkeleton();
|
|
61
|
+
const a = buildElement("section");
|
|
62
|
+
const b = buildElement("section");
|
|
63
|
+
b.id = a.id;
|
|
64
|
+
dup.sections.push(a, b);
|
|
65
|
+
check("duplicate ids fail validation", validatePage(dup).valid === false);
|
|
66
|
+
// reassignIds gives a fresh id
|
|
67
|
+
const before = a.id;
|
|
68
|
+
reassignIds(a);
|
|
69
|
+
check("reassignIds changes the id", a.id !== before);
|
|
70
|
+
// walk visits every node
|
|
71
|
+
let count = 0;
|
|
72
|
+
walk(src, () => {
|
|
73
|
+
count += 1;
|
|
74
|
+
});
|
|
75
|
+
check("walk visits all nodes", count === 3, count);
|
|
76
|
+
}
|
|
77
|
+
if (failures > 0) {
|
|
78
|
+
console.error(`\n${failures} smoke check(s) failed.`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
console.log("\nAll smoke checks passed.");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerAppTools(server, api, handle) {
|
|
3
|
+
server.tool("list_apps", "List all installed applications/subscriptions of the site. Returns app type, status, and settings", {}, () => handle(() => api.listApps()));
|
|
4
|
+
server.tool("get_app", "Get a specific installed app by type ID. Common types: 1=CMS, 2=Product Design, 10=Multilingual, etc.", {
|
|
5
|
+
type: z.string().describe("App type ID"),
|
|
6
|
+
}, ({ type }) => handle(() => api.getApp(type)));
|
|
7
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerArticleTools(server, api, handle) {
|
|
3
|
+
server.tool("list_articles", "List blog articles (metadata only, without HTML content). Use get_article to get full content", {
|
|
4
|
+
page: z.number().optional().describe("Page number"),
|
|
5
|
+
limit: z.number().optional().describe("Items per page"),
|
|
6
|
+
category_id: z.string().optional().describe("Filter by category"),
|
|
7
|
+
}, ({ page, limit, category_id }) => handle(async () => {
|
|
8
|
+
const res = await api.listArticles({ page, limit, category_id });
|
|
9
|
+
const articles = (res && res.data) || res || [];
|
|
10
|
+
if (!Array.isArray(articles))
|
|
11
|
+
return res;
|
|
12
|
+
return {
|
|
13
|
+
data: articles.map((a) => ({
|
|
14
|
+
id: a.id || a._id,
|
|
15
|
+
name: a.name,
|
|
16
|
+
slug: a.slug,
|
|
17
|
+
summary: a.summary || undefined,
|
|
18
|
+
category_id: a.category_id || undefined,
|
|
19
|
+
tags: a.tags || undefined,
|
|
20
|
+
is_hidden: a.is_hidden,
|
|
21
|
+
created_at: a.created_at,
|
|
22
|
+
updated_at: a.updated_at,
|
|
23
|
+
})),
|
|
24
|
+
total: res.total || articles.length,
|
|
25
|
+
};
|
|
26
|
+
}));
|
|
27
|
+
server.tool("get_article", "Get article details by ID", {
|
|
28
|
+
id: z.string().describe("Article ID"),
|
|
29
|
+
}, ({ id }) => handle(() => api.getArticle(id)));
|
|
30
|
+
server.tool("create_article", "Create a new blog article", {
|
|
31
|
+
name: z.string().describe("Article title"),
|
|
32
|
+
slug: z.string().describe("URL slug"),
|
|
33
|
+
content: z.string().describe("HTML content"),
|
|
34
|
+
summary: z.string().optional().describe("Summary"),
|
|
35
|
+
category_id: z.string().optional().describe("Category ID"),
|
|
36
|
+
tags: z.array(z.string()).optional().describe("Tags"),
|
|
37
|
+
images: z.array(z.string()).optional().describe("Image URLs"),
|
|
38
|
+
is_hidden: z.boolean().default(false).describe("Hide from public"),
|
|
39
|
+
}, (params) => handle(() => api.createArticle(params)));
|
|
40
|
+
server.tool("update_article", "Update a blog article", {
|
|
41
|
+
id: z.string().describe("Article ID"),
|
|
42
|
+
name: z.string().optional().describe("New title"),
|
|
43
|
+
slug: z.string().optional().describe("New slug"),
|
|
44
|
+
content: z.string().optional().describe("New HTML content"),
|
|
45
|
+
summary: z.string().optional().describe("New summary"),
|
|
46
|
+
category_id: z.string().optional().describe("Category ID"),
|
|
47
|
+
tags: z.array(z.string()).optional().describe("Tags"),
|
|
48
|
+
is_hidden: z.boolean().optional().describe("Hide from public"),
|
|
49
|
+
}, ({ id, ...params }) => handle(() => api.updateArticle(id, params)));
|
|
50
|
+
server.tool("delete_article", "Delete a blog article", {
|
|
51
|
+
id: z.string().describe("Article ID"),
|
|
52
|
+
}, ({ id }) => handle(() => api.deleteArticle(id)));
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerAutomationTools(server, api, handle) {
|
|
3
|
+
server.tool("send_mail", "Send email via CMS automation", {
|
|
4
|
+
to: z.string().describe("Recipient email"),
|
|
5
|
+
subject: z.string().describe("Email subject"),
|
|
6
|
+
body: z.string().describe("Email body (supports HTML)"),
|
|
7
|
+
}, (params) => handle(() => api.sendMail(params)));
|
|
8
|
+
}
|