webcake-landing-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 +535 -0
- package/dist/factory.js +421 -0
- package/dist/index.js +244 -0
- package/dist/library.js +372 -0
- package/dist/page-schema.json +266 -0
- package/dist/smoke.js +122 -0
- package/dist/validate.js +211 -0
- package/dist/webcake.js +238 -0
- package/package.json +42 -0
package/dist/webcake.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP client to talk to a Webcake backend:
|
|
3
|
+
* - list the account's organizations (GET /api/v1/org/organizations)
|
|
4
|
+
* - persist a generated page source (POST /api/v1/ai/create_page_from_source,
|
|
5
|
+
* added in lib/landing_page_web/controllers/v1/ai/ai_controller.ex)
|
|
6
|
+
*
|
|
7
|
+
* The page lands in an organization when an `x-org-id` header is sent (resolved
|
|
8
|
+
* by the backend `:org_check` plug). Without it the page is personal (org=null).
|
|
9
|
+
*
|
|
10
|
+
* Config via environment (set in the MCP server config):
|
|
11
|
+
* WEBCAKE_API_BASE e.g. http://localhost:5800 (required to call the backend)
|
|
12
|
+
* WEBCAKE_JWT the account JWT (required to call the backend)
|
|
13
|
+
* WEBCAKE_ORG_ID optional default organization id for create_page
|
|
14
|
+
* WEBCAKE_HOST optional Host header override (Phoenix routes by host)
|
|
15
|
+
* WEBCAKE_APP_BASE optional base for editor/preview URLs in the result
|
|
16
|
+
*/
|
|
17
|
+
const CREATE_ENDPOINT = "/api/v1/ai/create_page_from_source";
|
|
18
|
+
const ORGS_ENDPOINT = "/api/v1/org/organizations";
|
|
19
|
+
const PAGES_ENDPOINT = "/api/v1/ai/pages";
|
|
20
|
+
const PAGE_SOURCE_ENDPOINT = "/api/v1/ai/page_source";
|
|
21
|
+
const UPDATE_ENDPOINT = "/api/v1/ai/update_page_source";
|
|
22
|
+
export function readConfig() {
|
|
23
|
+
const base = process.env.WEBCAKE_API_BASE;
|
|
24
|
+
const jwt = process.env.WEBCAKE_JWT;
|
|
25
|
+
const missing = [];
|
|
26
|
+
if (!base)
|
|
27
|
+
missing.push("WEBCAKE_API_BASE");
|
|
28
|
+
if (!jwt)
|
|
29
|
+
missing.push("WEBCAKE_JWT");
|
|
30
|
+
if (missing.length)
|
|
31
|
+
return { config: null, missing };
|
|
32
|
+
return {
|
|
33
|
+
config: {
|
|
34
|
+
base: base.replace(/\/+$/, ""),
|
|
35
|
+
jwt: jwt,
|
|
36
|
+
orgId: process.env.WEBCAKE_ORG_ID,
|
|
37
|
+
host: process.env.WEBCAKE_HOST,
|
|
38
|
+
appBase: process.env.WEBCAKE_APP_BASE?.replace(/\/+$/, ""),
|
|
39
|
+
},
|
|
40
|
+
missing: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function authHeaders(config, orgId) {
|
|
44
|
+
const headers = {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Accept: "application/json",
|
|
47
|
+
Authorization: `Bearer ${config.jwt}`,
|
|
48
|
+
Cookie: `jwt=${config.jwt}`,
|
|
49
|
+
};
|
|
50
|
+
if (config.host)
|
|
51
|
+
headers["Host"] = config.host;
|
|
52
|
+
const org = orgId ?? config.orgId;
|
|
53
|
+
if (org != null && `${org}` !== "")
|
|
54
|
+
headers["x-org-id"] = `${org}`;
|
|
55
|
+
return headers;
|
|
56
|
+
}
|
|
57
|
+
/** Build (but do not send) the create request — used for dry-run previews. */
|
|
58
|
+
export function buildRequest(config, name, source, orgId) {
|
|
59
|
+
return {
|
|
60
|
+
method: "POST",
|
|
61
|
+
url: `${config.base}${CREATE_ENDPOINT}`,
|
|
62
|
+
headers: authHeaders(config, orgId),
|
|
63
|
+
body: JSON.stringify({ name, source }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Same as buildRequest but with the token masked, safe to show to the user. */
|
|
67
|
+
export function buildRequestRedacted(config, name, source, orgId) {
|
|
68
|
+
const req = buildRequest(config, name, source, orgId);
|
|
69
|
+
const mask = (s) => s.replace(config.jwt, "***JWT***");
|
|
70
|
+
return {
|
|
71
|
+
method: req.method,
|
|
72
|
+
url: req.url,
|
|
73
|
+
headers: { ...req.headers, Authorization: "Bearer ***JWT***", Cookie: "jwt=***JWT***" },
|
|
74
|
+
body: mask(req.body).slice(0, 400) + (req.body.length > 400 ? `… (${req.body.length} bytes)` : ""),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** List the account's organizations. type===1 is the default ("personal") org. */
|
|
78
|
+
export async function listOrganizations(config) {
|
|
79
|
+
const url = `${config.base}${ORGS_ENDPOINT}`;
|
|
80
|
+
let res;
|
|
81
|
+
try {
|
|
82
|
+
res = await fetch(url, { method: "GET", headers: authHeaders(config) });
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return { ok: false, status: 0, error: `Network error calling ${url}: ${e?.message ?? e}` };
|
|
86
|
+
}
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
let json = null;
|
|
89
|
+
try {
|
|
90
|
+
json = JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* non-JSON */
|
|
94
|
+
}
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
return { ok: false, status: res.status, error: `Backend returned ${res.status}: ${text.slice(0, 300)}` };
|
|
97
|
+
}
|
|
98
|
+
const list = json?.organizations ?? json?.data ?? [];
|
|
99
|
+
const organizations = list.map((o) => ({
|
|
100
|
+
id: o.id,
|
|
101
|
+
name: o.name,
|
|
102
|
+
type: o.type ?? null,
|
|
103
|
+
is_default: o.type === 1,
|
|
104
|
+
}));
|
|
105
|
+
const def = organizations.find((o) => o.is_default);
|
|
106
|
+
return { ok: true, status: res.status, organizations, default_org_id: def?.id };
|
|
107
|
+
}
|
|
108
|
+
/** Actually POST the source. Requires global fetch (Node 18+). */
|
|
109
|
+
export async function createPage(config, name, source, orgId) {
|
|
110
|
+
const req = buildRequest(config, name, source, orgId);
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await fetch(req.url, { method: req.method, headers: req.headers, body: req.body });
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
return { ok: false, status: 0, error: `Network error calling ${req.url}: ${e?.message ?? e}` };
|
|
117
|
+
}
|
|
118
|
+
const text = await res.text();
|
|
119
|
+
let json = null;
|
|
120
|
+
try {
|
|
121
|
+
json = JSON.parse(text);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* non-JSON response */
|
|
125
|
+
}
|
|
126
|
+
const data = json?.data ?? json;
|
|
127
|
+
const pageId = data?.page_id;
|
|
128
|
+
const editorPath = data?.editor_url;
|
|
129
|
+
const previewPath = data?.preview_url;
|
|
130
|
+
const app = config.appBase;
|
|
131
|
+
if (!res.ok || !pageId) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
status: res.status,
|
|
135
|
+
raw: json ?? text.slice(0, 600),
|
|
136
|
+
error: `Backend returned ${res.status}${pageId ? "" : " (no page_id in response)"}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
status: res.status,
|
|
142
|
+
page_id: pageId,
|
|
143
|
+
editor_url: app && editorPath ? `${app}${editorPath}` : editorPath,
|
|
144
|
+
preview_url: app && previewPath ? `${app}${previewPath}` : previewPath,
|
|
145
|
+
organization_id: (orgId ?? config.orgId) ?? null,
|
|
146
|
+
raw: data,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function getJson(url, config) {
|
|
150
|
+
let res;
|
|
151
|
+
try {
|
|
152
|
+
res = await fetch(url, { method: "GET", headers: authHeaders(config) });
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
return { ok: false, status: 0, json: null, text: "", error: `Network error calling ${url}: ${e?.message ?? e}` };
|
|
156
|
+
}
|
|
157
|
+
const text = await res.text();
|
|
158
|
+
let json = null;
|
|
159
|
+
try {
|
|
160
|
+
json = JSON.parse(text);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
/* non-JSON */
|
|
164
|
+
}
|
|
165
|
+
return { ok: res.ok, status: res.status, json, text, error: res.ok ? undefined : `Backend returned ${res.status}: ${text.slice(0, 300)}` };
|
|
166
|
+
}
|
|
167
|
+
/** List pages owned by the account (most-recent first). */
|
|
168
|
+
export async function listPages(config) {
|
|
169
|
+
const r = await getJson(`${config.base}${PAGES_ENDPOINT}`, config);
|
|
170
|
+
if (!r.ok)
|
|
171
|
+
return { ok: false, status: r.status, error: r.error };
|
|
172
|
+
const pages = r.json?.data?.pages ?? r.json?.pages ?? [];
|
|
173
|
+
return { ok: true, status: r.status, pages };
|
|
174
|
+
}
|
|
175
|
+
/** Read a page's decoded source tree (must be owned by the account). */
|
|
176
|
+
export async function getPageSource(config, pageId) {
|
|
177
|
+
const url = `${config.base}${PAGE_SOURCE_ENDPOINT}?page_id=${encodeURIComponent(pageId)}`;
|
|
178
|
+
const r = await getJson(url, config);
|
|
179
|
+
if (!r.ok)
|
|
180
|
+
return { ok: false, status: r.status, error: r.error };
|
|
181
|
+
const d = r.json?.data ?? r.json ?? {};
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
status: r.status,
|
|
185
|
+
page_id: d.page_id,
|
|
186
|
+
name: d.name,
|
|
187
|
+
organization_id: d.organization_id ?? null,
|
|
188
|
+
source: d.source,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Build (but do not send) the update request — for dry-run previews. */
|
|
192
|
+
export function buildUpdateRequestRedacted(config, pageId, source) {
|
|
193
|
+
const body = JSON.stringify({ page_id: pageId, source });
|
|
194
|
+
return {
|
|
195
|
+
method: "POST",
|
|
196
|
+
url: `${config.base}${UPDATE_ENDPOINT}`,
|
|
197
|
+
headers: { ...authHeaders(config), Authorization: "Bearer ***JWT***", Cookie: "jwt=***JWT***" },
|
|
198
|
+
body: body.replace(config.jwt, "***JWT***").slice(0, 400) + (body.length > 400 ? `… (${body.length} bytes)` : ""),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Overwrite an existing page's source (source-only). */
|
|
202
|
+
export async function updatePageSource(config, pageId, source) {
|
|
203
|
+
const url = `${config.base}${UPDATE_ENDPOINT}`;
|
|
204
|
+
let res;
|
|
205
|
+
try {
|
|
206
|
+
res = await fetch(url, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: authHeaders(config),
|
|
209
|
+
body: JSON.stringify({ page_id: pageId, source }),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return { ok: false, status: 0, error: `Network error calling ${url}: ${e?.message ?? e}` };
|
|
214
|
+
}
|
|
215
|
+
const text = await res.text();
|
|
216
|
+
let json = null;
|
|
217
|
+
try {
|
|
218
|
+
json = JSON.parse(text);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* non-JSON */
|
|
222
|
+
}
|
|
223
|
+
const data = json?.data ?? json;
|
|
224
|
+
const pageIdOut = data?.page_id;
|
|
225
|
+
const app = config.appBase;
|
|
226
|
+
if (!res.ok || !pageIdOut) {
|
|
227
|
+
return { ok: false, status: res.status, raw: json ?? text.slice(0, 600), error: `Backend returned ${res.status}` };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
status: res.status,
|
|
232
|
+
page_id: pageIdOut,
|
|
233
|
+
editor_url: app && data?.editor_url ? `${app}${data.editor_url}` : data?.editor_url,
|
|
234
|
+
preview_url: app && data?.preview_url ? `${app}${data.preview_url}` : data?.preview_url,
|
|
235
|
+
organization_id: data?.organization_id ?? null,
|
|
236
|
+
raw: data,
|
|
237
|
+
};
|
|
238
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webcake-landing-mcp",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"webcake-landing-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/vuluu2k/webcake-landing-mcp.git"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc && node -e \"require('fs').copyFileSync('src/page-schema.json','dist/page-schema.json')\"",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"smoke": "node dist/smoke.js",
|
|
24
|
+
"prepare": "npm run build",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run smoke",
|
|
26
|
+
"release": "node scripts/release.js",
|
|
27
|
+
"release:patch": "node scripts/release.js patch",
|
|
28
|
+
"release:minor": "node scripts/release.js minor",
|
|
29
|
+
"release:major": "node scripts/release.js major",
|
|
30
|
+
"release:fast": "node scripts/release.js patch --skip-checks",
|
|
31
|
+
"release:dry": "node scripts/release.js patch --dry-run"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
35
|
+
"ajv": "^8.17.1",
|
|
36
|
+
"zod": "^3.23.8"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|