webcake-landing-mcp 1.0.8 → 1.0.10
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 +109 -24
- package/README.vi.md +187 -17
- package/dist/auth/login.js +13 -20
- package/dist/http.js +36 -3
- package/dist/index.js +33 -0
- package/dist/install.js +77 -38
- package/dist/persistence/config.js +32 -4
- package/dist/smoke.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,15 +54,18 @@ valid element/page skeletons, a page validator, and tools to create or edit page
|
|
|
54
54
|
The AI agent produces the full `{ page, popup, settings, options, cartConfigs }` JSON; `create_page`
|
|
55
55
|
persists it (source-only — the page opens in the editor where re-saving renders it).
|
|
56
56
|
|
|
57
|
-
##
|
|
57
|
+
## Setup methods (pick one)
|
|
58
58
|
|
|
59
|
-
|
|
|
60
|
-
|
|
61
|
-
|
|
|
62
|
-
|
|
|
59
|
+
| # | Method | Best for | Auth | Jump to |
|
|
60
|
+
|---|--------|----------|------|---------|
|
|
61
|
+
| 1 | **Local stdio** — add to an IDE (Claude Desktop / Cursor / …) via `npx` or a built file | Daily use on your machine | env `WEBCAKE_JWT`, or `login`, or none (reference tools) | [IDE config](#configuration-by-ide--ai-tool) |
|
|
62
|
+
| 2 | **`login`** — grab the token through the browser (no copy-paste) | Avoiding a manual token paste (stdio / single-user remote) | browser session → saved `auth.json` | [Connect once](#connect-once--grab-your-token-automatically-login) |
|
|
63
|
+
| 3 | **Remote HTTP (`serve`)** — run as an HTTP server, test with MCP Inspector / `mcp-remote` / curl | Trying the remote transport locally | per-request `x-webcake-jwt` header, or env | [Remote](#run-as-a-remote-connector-streamable-http) |
|
|
64
|
+
| 4 | **VPS + claude.ai connector** — deploy public HTTPS, add as a custom connector | Sharing one hosted server | single-account (env token); per-user needs OAuth (not implemented) | [Deploy on a VPS](#deploy-on-a-vps) |
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
Two **run forms** apply to any method: **`npx -y webcake-landing-mcp …`** (no clone, auto-updates) or **`node /abs/path/dist/index.js …`** (a cloned build — run `npm run build` first). The IDE configs below show the local form; swap `command`/`args` for the npx form to use CDN mode.
|
|
67
|
+
|
|
68
|
+
The **reference + generation tools** (`get_generation_guide`, `list_elements`, `validate_page`, …) work with **zero config**; only the **persistence tools** (`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizations`) need a token. Credentials resolve in order: **per-request header → env var → saved `auth.json`** (`login`).
|
|
66
69
|
|
|
67
70
|
## Quick Install (Recommended)
|
|
68
71
|
|
|
@@ -144,14 +147,14 @@ npx -y github:vuluu2k/webcake-landing-mcp
|
|
|
144
147
|
config into your IDE. The bundled `install` subcommand does that step for you, no clone needed:
|
|
145
148
|
|
|
146
149
|
```bash
|
|
147
|
-
# Interactive —
|
|
150
|
+
# Interactive — pick environment, log in via browser (or paste a JWT), pick IDE(s)
|
|
148
151
|
npx -y webcake-landing-mcp install
|
|
149
152
|
|
|
150
|
-
# Non-interactive — configure every supported IDE at once
|
|
151
|
-
npx -y webcake-landing-mcp install --ide all --jwt <your-jwt>
|
|
153
|
+
# Non-interactive — configure every supported IDE at once (env + token via flags)
|
|
154
|
+
npx -y webcake-landing-mcp install --ide all --env prod --jwt <your-jwt>
|
|
152
155
|
|
|
153
|
-
#
|
|
154
|
-
npx -y webcake-landing-mcp install --ide cursor --jwt <your-jwt>
|
|
156
|
+
# Local dev — point at your local stack (localhost:5800 / :5173)
|
|
157
|
+
npx -y webcake-landing-mcp install --ide cursor --env local --jwt <your-jwt>
|
|
155
158
|
|
|
156
159
|
# Remove the server from every IDE config
|
|
157
160
|
npx -y webcake-landing-mcp uninstall
|
|
@@ -159,8 +162,10 @@ npx -y webcake-landing-mcp uninstall
|
|
|
159
162
|
|
|
160
163
|
It writes a `webcake-landing` entry (using the `npx` launch form below) into the right config file
|
|
161
164
|
for each target: `claude-desktop`, `claude-code`, `cursor`, `windsurf`, `augment` (VS Code), `codex`,
|
|
162
|
-
or `all`.
|
|
163
|
-
|
|
165
|
+
or `all`. Interactively it asks for the **environment** (`local`/`staging`/`prod`, which sets the API +
|
|
166
|
+
app URLs) and whether to **log in via the browser or paste a JWT**. Flags: `--ide`, `--env`, `--jwt`,
|
|
167
|
+
`--org-id`, `--api-base`/`--app-base`/`--host` (advanced overrides), `--npx`/`--local`, `-y`. Run
|
|
168
|
+
`npx -y webcake-landing-mcp install --help` for the full list.
|
|
164
169
|
|
|
165
170
|
### Manual config
|
|
166
171
|
|
|
@@ -173,7 +178,7 @@ The MCP config is the same as the local one, but `command`/`args` point at `npx`
|
|
|
173
178
|
"command": "npx",
|
|
174
179
|
"args": ["-y", "webcake-landing-mcp"],
|
|
175
180
|
"env": {
|
|
176
|
-
"
|
|
181
|
+
"WEBCAKE_ENV": "prod",
|
|
177
182
|
"WEBCAKE_JWT": "<your-jwt>"
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -202,6 +207,12 @@ host), then in Claude → **Add custom connector**:
|
|
|
202
207
|
- **Name**: `webcake-landing`
|
|
203
208
|
- **Remote MCP server URL**: `https://<your-host>/mcp`
|
|
204
209
|
|
|
210
|
+
The dialog has no header field, so to pass a token through it, **put it in the URL**:
|
|
211
|
+
`https://<your-host>/mcp?jwt=<ljwt>` (also accepts `&api_base=…`, `&org_id=…`, `&host=…`, `&app_base=…`).
|
|
212
|
+
Give each person a URL with their own `jwt` → **per-user without OAuth**. An explicit `x-webcake-jwt` header
|
|
213
|
+
still wins over the query. ⚠️ A token in a URL can land in access/proxy logs — require **HTTPS** and disable
|
|
214
|
+
query-string logging on your reverse proxy; a header (or OAuth) is safer when the client supports it.
|
|
215
|
+
|
|
205
216
|
### Auth — per-request, multi-user (no shared token)
|
|
206
217
|
|
|
207
218
|
In stdio mode the JWT comes from env. In HTTP mode each request carries the caller's **own** credentials
|
|
@@ -222,9 +233,54 @@ by setting `WEBCAKE_API_BASE` + `WEBCAKE_JWT` in the host's env and keeping the
|
|
|
222
233
|
> no secret; only the persistence tools (`create_page`, `update_page`, …) use the JWT. If a request has no
|
|
223
234
|
> JWT, those tools return `missing_env` instead of touching the network.
|
|
224
235
|
>
|
|
225
|
-
> Note: the
|
|
226
|
-
>
|
|
227
|
-
>
|
|
236
|
+
> Note: the claude.ai connector dialog has **no header field** (only OAuth, which this server does not
|
|
237
|
+
> implement yet). Two ways around it: put the token in the URL as `?jwt=<ljwt>` (above — per-user, but the
|
|
238
|
+
> token shows up in logs), or use a header-capable client (`mcp-remote --header …`, below). A token in the
|
|
239
|
+
> server's env instead gives a shared **single-account** for everyone on that URL.
|
|
240
|
+
|
|
241
|
+
### Test it locally (no public URL needed)
|
|
242
|
+
|
|
243
|
+
`localhost` can't be used in the claude.ai dialog (Anthropic fetches the URL from its own servers). To try the
|
|
244
|
+
running `serve` server on your machine:
|
|
245
|
+
|
|
246
|
+
- **MCP Inspector** (GUI — easiest): `npx @modelcontextprotocol/inspector` → Transport **Streamable HTTP** →
|
|
247
|
+
URL `http://localhost:8787/mcp` → under Headers add `x-webcake-jwt` (+ `x-webcake-api-base`) → Connect → call tools.
|
|
248
|
+
- **`mcp-remote`** (use the remote server from a stdio client like Claude Desktop, with headers):
|
|
249
|
+
```json
|
|
250
|
+
{ "mcpServers": { "webcake-remote": { "command": "npx",
|
|
251
|
+
"args": ["-y", "mcp-remote", "http://localhost:8787/mcp",
|
|
252
|
+
"--header", "x-webcake-jwt:<ljwt>",
|
|
253
|
+
"--header", "x-webcake-api-base:https://api.webcake.io"] } } }
|
|
254
|
+
```
|
|
255
|
+
- **curl**: `initialize` (read the `mcp-session-id` response header) → `tools/list` → `tools/call`, all with
|
|
256
|
+
`Accept: application/json, text/event-stream`.
|
|
257
|
+
|
|
258
|
+
### Deploy on a VPS
|
|
259
|
+
|
|
260
|
+
1. **Build + run as a service** — `/etc/systemd/system/webcake-mcp.service`:
|
|
261
|
+
```ini
|
|
262
|
+
[Service]
|
|
263
|
+
WorkingDirectory=/opt/webcake-landing-mcp
|
|
264
|
+
ExecStart=/usr/bin/node dist/index.js serve --port 8787
|
|
265
|
+
Environment=WEBCAKE_API_BASE=https://api.webcake.io
|
|
266
|
+
Environment=WEBCAKE_JWT=<ljwt> # single-account only — see auth note below
|
|
267
|
+
Restart=always
|
|
268
|
+
[Install]
|
|
269
|
+
WantedBy=multi-user.target
|
|
270
|
+
```
|
|
271
|
+
`sudo systemctl enable --now webcake-mcp` (build once: `npm install && npm run build`).
|
|
272
|
+
2. **HTTPS + domain** (claude.ai requires https) — e.g. Caddy auto-TLS, `/etc/caddy/Caddyfile`:
|
|
273
|
+
```
|
|
274
|
+
mcp.yourdomain.com { reverse_proxy localhost:8787 }
|
|
275
|
+
```
|
|
276
|
+
3. **Add to claude.ai** → Remote MCP server URL = `https://mcp.yourdomain.com/mcp`.
|
|
277
|
+
|
|
278
|
+
**Auth on a shared server:**
|
|
279
|
+
- **Single-account** (works with the dialog today): `WEBCAKE_JWT` in the service env → everyone using the
|
|
280
|
+
connector shares that one Webcake account. Keep the URL private / gated; the token expires (~90 days).
|
|
281
|
+
- **Per-user** (each person their own account): give each person a URL with their own `?jwt=<ljwt>` (works
|
|
282
|
+
through the dialog, but the token appears in logs), or use a header-capable client (`mcp-remote --header …`),
|
|
283
|
+
or add **OAuth** (not implemented) for the cleanest flow.
|
|
228
284
|
|
|
229
285
|
## Manual Setup (local)
|
|
230
286
|
|
|
@@ -247,7 +303,11 @@ Instead of copying a JWT by hand, run:
|
|
|
247
303
|
# Production — zero config (defaults: connect via webcake.io, API via api.webcake.io):
|
|
248
304
|
npx -y webcake-landing-mcp login
|
|
249
305
|
|
|
250
|
-
# Local dev —
|
|
306
|
+
# Local dev / staging — pick a named environment (see Environments below):
|
|
307
|
+
node dist/index.js login --env local # SPA :5173 + API :5800
|
|
308
|
+
node dist/index.js login --env staging # staging.webcake.io + api.staging.webcake.io
|
|
309
|
+
|
|
310
|
+
# …or point at custom URLs explicitly (these override --env):
|
|
251
311
|
node dist/index.js login \
|
|
252
312
|
--connect-url http://localhost:5173/mcp-connect \
|
|
253
313
|
--api-base http://localhost:5800
|
|
@@ -257,9 +317,9 @@ It opens your browser → (log into Webcake if needed) → the token is saved to
|
|
|
257
317
|
`~/.webcake-landing-mcp/auth.json`, which the server then reads automatically.
|
|
258
318
|
|
|
259
319
|
You're already logged in to Webcake in your browser, so `login` just opens a Webcake "connect"
|
|
260
|
-
page that reads your
|
|
320
|
+
page that reads your **`ljwt`** (landing) cookie and hands the token back to a localhost callback —
|
|
261
321
|
no copy-paste. The saved token is used by **both** the stdio server and a single-user `serve`
|
|
262
|
-
deployment (env vars still take precedence).
|
|
322
|
+
deployment (env vars still take precedence). The landing JWT lasts ~90 days, so you rarely reconnect.
|
|
263
323
|
|
|
264
324
|
Two URLs, don't mix them up:
|
|
265
325
|
|
|
@@ -276,12 +336,14 @@ Other flags: `--org-id`, `--port`, `--no-open`. Saved-file dir: `WEBCAKE_CONFIG_
|
|
|
276
336
|
|
|
277
337
|
```
|
|
278
338
|
GET /mcp-connect?redirect_uri=<loopback>&state=<s>
|
|
279
|
-
→ read the `
|
|
280
|
-
→ 302 to <redirect_uri>?token=<
|
|
339
|
+
→ read the `ljwt` cookie (the logged-in user's landing token)
|
|
340
|
+
→ 302 to <redirect_uri>?token=<ljwt>&state=<s>
|
|
281
341
|
(if there's no cookie: 302 to the login page first, then back here)
|
|
282
342
|
```
|
|
283
343
|
|
|
284
344
|
For safety, only honor `redirect_uri` values on `http://127.0.0.1:*` / `http://localhost:*`.
|
|
345
|
+
(Reference implementation: `builderx_spa/src/views/McpConnect.vue` reads `cookies.get('ljwt')` — so this
|
|
346
|
+
flow can also be done entirely in the SPA, no backend route needed.)
|
|
285
347
|
|
|
286
348
|
> Multi-user remote (the claude.ai connector dialog) can't do this browser loopback — there each
|
|
287
349
|
> user sends their own token via the `x-webcake-jwt` header (see the remote-connector section above).
|
|
@@ -290,7 +352,8 @@ For safety, only honor `redirect_uri` values on `http://127.0.0.1:*` / `http://l
|
|
|
290
352
|
|
|
291
353
|
| Variable | Required | Description |
|
|
292
354
|
|----------|----------|-------------|
|
|
293
|
-
| `
|
|
355
|
+
| `WEBCAKE_ENV` | No | Named environment: `local` \| `staging` \| `prod`. Fills in `WEBCAKE_API_BASE` + `WEBCAKE_APP_BASE` from a preset (see table below). Also settable with the `--env <name>` flag. Explicit vars win. |
|
|
356
|
+
| `WEBCAKE_API_BASE` | No* | Backend base URL, e.g. `http://localhost:5800`. Required to persist (or set `WEBCAKE_ENV`). |
|
|
294
357
|
| `WEBCAKE_JWT` | No* | Account JWT (dashboard auth). Required to persist — expires, refresh when needed. |
|
|
295
358
|
| `WEBCAKE_ORG_ID` | No | Default organization id for `create_page` (overridden by its `organization_id` arg). Omit → personal page. |
|
|
296
359
|
| `WEBCAKE_HOST` | No | Optional `Host` header (Phoenix routes by host, e.g. `builder.localhost`). |
|
|
@@ -304,6 +367,28 @@ For safety, only honor `redirect_uri` values on `http://127.0.0.1:*` / `http://l
|
|
|
304
367
|
> Persisting writes a real page to whatever `WEBCAKE_API_BASE` points at, using the JWT as that account.
|
|
305
368
|
> Start against local/staging.
|
|
306
369
|
|
|
370
|
+
### Environments (`--env` / `WEBCAKE_ENV`)
|
|
371
|
+
|
|
372
|
+
Instead of setting both base URLs by hand, pick a named environment — one source of
|
|
373
|
+
truth for the API + app bases:
|
|
374
|
+
|
|
375
|
+
| `--env` / `WEBCAKE_ENV` | API base (`WEBCAKE_API_BASE`) | App base (`WEBCAKE_APP_BASE`) |
|
|
376
|
+
|-------------------------|-------------------------------|-------------------------------|
|
|
377
|
+
| `local` | `http://localhost:5800` | `http://localhost:5173` |
|
|
378
|
+
| `staging` | `https://api.staging.webcake.io` | `https://staging.webcake.io` |
|
|
379
|
+
| `prod` | `https://api.webcake.io` | `https://webcake.io` |
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
node dist/index.js serve --env staging # remote server on the staging backend
|
|
383
|
+
node dist/index.js login --env local # connect against your local SPA + API
|
|
384
|
+
WEBCAKE_ENV=prod node dist/index.js # stdio, prod (env var form)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Explicit `WEBCAKE_API_BASE` / `WEBCAKE_APP_BASE` (or `--api-base`) still override the
|
|
388
|
+
preset, field by field. On the remote HTTP server a client can override the server's
|
|
389
|
+
environment per request with the **`x-webcake-env`** header or **`?env=`** query
|
|
390
|
+
(e.g. `…/mcp?jwt=<token>&env=staging`) — so one server can serve multiple environments.
|
|
391
|
+
|
|
307
392
|
### How to get `WEBCAKE_JWT`
|
|
308
393
|
|
|
309
394
|
1. Open the WebCake builder dashboard and log in
|
package/README.vi.md
CHANGED
|
@@ -55,15 +55,18 @@ element/trang hợp lệ, bộ kiểm tra trang, và các tool để tạo/sửa
|
|
|
55
55
|
`{ page, popup, settings, options, cartConfigs }`; `create_page` lưu nó (chỉ-source — trang mở trong editor,
|
|
56
56
|
lưu lại sẽ render).
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## Các cách setup (chọn một)
|
|
59
59
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
|
63
|
-
|
|
|
60
|
+
| # | Cách | Hợp cho | Auth | Xem |
|
|
61
|
+
|---|------|---------|------|-----|
|
|
62
|
+
| 1 | **Local stdio** — gắn vào IDE (Claude Desktop / Cursor / …) qua `npx` hoặc file build | Dùng hằng ngày trên máy | env `WEBCAKE_JWT`, hoặc `login`, hoặc không cần (tool tham chiếu) | [Cấu hình IDE](#cấu-hình-theo-ide--công-cụ-ai) |
|
|
63
|
+
| 2 | **`login`** — tự lấy token qua browser (khỏi copy-paste) | Khỏi dán token tay (stdio / remote 1 người) | session browser → file `auth.json` | [Kết nối một lần](#kết-nối-một-lần--tự-lấy-token-login) |
|
|
64
|
+
| 3 | **Remote HTTP (`serve`)** — chạy như HTTP server, test bằng MCP Inspector / `mcp-remote` / curl | Thử transport remote ở local | header `x-webcake-jwt` mỗi request, hoặc env | [Remote](#chạy-như-remote-connector-streamable-http) |
|
|
65
|
+
| 4 | **VPS + claude.ai connector** — deploy HTTPS public, thêm làm custom connector | Chia sẻ 1 server hosted | single-account (token env); per-user cần OAuth (chưa có) | [Deploy lên VPS](#deploy-lên-vps) |
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
Hai **dạng chạy** áp dụng cho mọi cách: **`npx -y webcake-landing-mcp …`** (không clone, tự cập nhật) hoặc **`node /abs/path/dist/index.js …`** (bản đã clone & build — chạy `npm run build` trước). Cấu hình IDE bên dưới dùng dạng local; đổi `command`/`args` sang dạng npx để dùng CDN.
|
|
68
|
+
|
|
69
|
+
Các **tool tham chiếu + generation** (`get_generation_guide`, `list_elements`, `validate_page`, …) chạy **zero config**; chỉ **tool lưu trữ** (`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizations`) mới cần token. Token ưu tiên theo thứ tự: **header mỗi request → biến env → file `auth.json`** (`login`).
|
|
67
70
|
|
|
68
71
|
## Cài nhanh (Khuyến nghị)
|
|
69
72
|
|
|
@@ -144,23 +147,25 @@ npx -y github:vuluu2k/webcake-landing-mcp
|
|
|
144
147
|
Lệnh con `install` đi kèm sẽ làm hộ bạn bước đó, không cần clone:
|
|
145
148
|
|
|
146
149
|
```bash
|
|
147
|
-
# Tương tác —
|
|
150
|
+
# Tương tác — chọn môi trường, đăng nhập qua trình duyệt (hoặc dán JWT), chọn IDE
|
|
148
151
|
npx -y webcake-landing-mcp install
|
|
149
152
|
|
|
150
|
-
# Không tương tác — cấu hình mọi IDE hỗ trợ cùng lúc
|
|
151
|
-
npx -y webcake-landing-mcp install --ide all --jwt <your-jwt>
|
|
153
|
+
# Không tương tác — cấu hình mọi IDE hỗ trợ cùng lúc (env + token qua cờ)
|
|
154
|
+
npx -y webcake-landing-mcp install --ide all --env prod --jwt <your-jwt>
|
|
152
155
|
|
|
153
|
-
#
|
|
154
|
-
npx -y webcake-landing-mcp install --ide cursor --jwt <your-jwt>
|
|
156
|
+
# Local dev — trỏ vào stack local của bạn (localhost:5800 / :5173)
|
|
157
|
+
npx -y webcake-landing-mcp install --ide cursor --env local --jwt <your-jwt>
|
|
155
158
|
|
|
156
159
|
# Gỡ server khỏi mọi cấu hình IDE
|
|
157
160
|
npx -y webcake-landing-mcp uninstall
|
|
158
161
|
```
|
|
159
162
|
|
|
160
163
|
Nó ghi entry `webcake-landing` (dùng dạng khởi chạy `npx` bên dưới) vào đúng file cấu hình của từng IDE:
|
|
161
|
-
`claude-desktop`, `claude-code`, `cursor`, `windsurf`, `augment` (VS Code), `codex`, hoặc `all`.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
`claude-desktop`, `claude-code`, `cursor`, `windsurf`, `augment` (VS Code), `codex`, hoặc `all`. Khi tương
|
|
165
|
+
tác, nó hỏi **môi trường** (`local`/`staging`/`prod` — mặc định `prod`, dùng để đặt API + app URL) và cho
|
|
166
|
+
chọn **đăng nhập qua trình duyệt hay dán JWT**. Cờ: `--ide`, `--env`, `--jwt`, `--org-id`,
|
|
167
|
+
`--api-base`/`--app-base`/`--host` (ghi đè nâng cao), `--npx`/`--local`, `-y`. Chạy
|
|
168
|
+
`npx -y webcake-landing-mcp install --help` để xem đầy đủ.
|
|
164
169
|
|
|
165
170
|
### Cấu hình thủ công
|
|
166
171
|
|
|
@@ -173,7 +178,7 @@ Cấu hình MCP giống bản local, chỉ khác `command`/`args` trỏ tới `n
|
|
|
173
178
|
"command": "npx",
|
|
174
179
|
"args": ["-y", "webcake-landing-mcp"],
|
|
175
180
|
"env": {
|
|
176
|
-
"
|
|
181
|
+
"WEBCAKE_ENV": "prod",
|
|
177
182
|
"WEBCAKE_JWT": "<your-jwt>"
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -184,6 +189,97 @@ Cấu hình MCP giống bản local, chỉ khác `command`/`args` trỏ tới `n
|
|
|
184
189
|
> npx cache lại package sau lần chạy đầu, nên các lần sau khởi động nhanh. Dùng phiên bản ghim
|
|
185
190
|
> (`webcake-landing-mcp@1.0.0`) nếu cần build tái lập được.
|
|
186
191
|
|
|
192
|
+
## Chạy như remote connector (Streamable HTTP)
|
|
193
|
+
|
|
194
|
+
Server còn nói được transport **remote MCP** (Streamable HTTP), nên có thể thêm qua dialog
|
|
195
|
+
**"Add custom connector"** của Claude bằng một URL — không chỉ stdio local.
|
|
196
|
+
|
|
197
|
+
Chạy chế độ HTTP (port mặc định `8787`, hoặc đặt `PORT` / `--port`):
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npx -y webcake-landing-mcp serve --port 8787
|
|
201
|
+
# → endpoint MCP tại http://localhost:8787/mcp (GET / hoặc /health trả JSON trạng thái)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Đưa ra **HTTPS** ở URL public (reverse proxy, tunnel như `ngrok http 8787`, hoặc host bất kỳ), rồi vào
|
|
205
|
+
Claude → **Add custom connector**:
|
|
206
|
+
|
|
207
|
+
- **Name**: `webcake-landing`
|
|
208
|
+
- **Remote MCP server URL**: `https://<host-của-bạn>/mcp`
|
|
209
|
+
|
|
210
|
+
Dialog không có ô header, nên muốn truyền token qua đó thì **để vào URL**:
|
|
211
|
+
`https://<host-của-bạn>/mcp?jwt=<ljwt>` (nhận thêm `&api_base=…`, `&org_id=…`, `&host=…`, `&app_base=…`).
|
|
212
|
+
Mỗi người một URL với `jwt` riêng → **per-user mà không cần OAuth**. Header `x-webcake-jwt` thật vẫn ưu tiên
|
|
213
|
+
hơn query. ⚠️ Token nằm trong URL có thể lọt vào access/proxy log — **bắt buộc HTTPS** và tắt log query ở
|
|
214
|
+
reverse proxy; dùng header (hoặc OAuth) an toàn hơn nếu client hỗ trợ.
|
|
215
|
+
|
|
216
|
+
### Auth — mỗi request, đa người dùng (không token chung)
|
|
217
|
+
|
|
218
|
+
Ở stdio JWT lấy từ env. Ở chế độ HTTP, mỗi request mang credential **riêng** của người gọi qua header,
|
|
219
|
+
nên server hosted là đa người dùng và không nhúng secret chung:
|
|
220
|
+
|
|
221
|
+
| Header | Tương ứng | Ghi chú |
|
|
222
|
+
|--------|-----------|---------|
|
|
223
|
+
| `x-webcake-jwt` (hoặc `Authorization: Bearer <jwt>`) | `WEBCAKE_JWT` | token tài khoản — gửi mỗi request |
|
|
224
|
+
| `x-webcake-org-id` | `WEBCAKE_ORG_ID` | org mặc định |
|
|
225
|
+
| `x-webcake-api-base` | `WEBCAKE_API_BASE` | thường set 1 lần qua env trên host |
|
|
226
|
+
| `x-webcake-app-base` | `WEBCAKE_APP_BASE` | base URL editor/preview |
|
|
227
|
+
|
|
228
|
+
Header nào thiếu thì fallback về biến env tương ứng — nên cũng chạy **một người dùng** được bằng cách đặt
|
|
229
|
+
`WEBCAKE_API_BASE` + `WEBCAKE_JWT` trong env của host và giữ URL riêng tư.
|
|
230
|
+
|
|
231
|
+
> ⚠️ Tool tham chiếu + generation (`get_generation_guide`, `list_elements`, `validate_page`, …) không cần
|
|
232
|
+
> secret; chỉ tool lưu trữ (`create_page`, `update_page`, …) dùng JWT. Request không có JWT thì các tool đó
|
|
233
|
+
> trả `missing_env` chứ không gọi mạng.
|
|
234
|
+
>
|
|
235
|
+
> Lưu ý: dialog claude.ai **không có ô header** (chỉ có OAuth, mà server này **chưa làm**). Hai cách lách:
|
|
236
|
+
> để token vào URL dạng `?jwt=<ljwt>` (như trên — per-user, nhưng token lộ trong log), hoặc dùng client hỗ trợ
|
|
237
|
+
> header (`mcp-remote --header …`, bên dưới). Đặt token ở env server thì thành **single-account** chung cho mọi người trên URL đó.
|
|
238
|
+
|
|
239
|
+
### Test ở local (không cần URL public)
|
|
240
|
+
|
|
241
|
+
`localhost` không dùng được trong dialog claude.ai (Anthropic gọi URL từ server của họ). Để thử server `serve`
|
|
242
|
+
chạy trên máy:
|
|
243
|
+
|
|
244
|
+
- **MCP Inspector** (GUI — dễ nhất): `npx @modelcontextprotocol/inspector` → Transport **Streamable HTTP** →
|
|
245
|
+
URL `http://localhost:8787/mcp` → mục Headers thêm `x-webcake-jwt` (+ `x-webcake-api-base`) → Connect → bấm gọi tool.
|
|
246
|
+
- **`mcp-remote`** (dùng server remote từ client stdio như Claude Desktop, kèm header):
|
|
247
|
+
```json
|
|
248
|
+
{ "mcpServers": { "webcake-remote": { "command": "npx",
|
|
249
|
+
"args": ["-y", "mcp-remote", "http://localhost:8787/mcp",
|
|
250
|
+
"--header", "x-webcake-jwt:<ljwt>",
|
|
251
|
+
"--header", "x-webcake-api-base:https://api.webcake.io"] } } }
|
|
252
|
+
```
|
|
253
|
+
- **curl**: `initialize` (đọc header `mcp-session-id` trả về) → `tools/list` → `tools/call`, tất cả kèm
|
|
254
|
+
`Accept: application/json, text/event-stream`.
|
|
255
|
+
|
|
256
|
+
### Deploy lên VPS
|
|
257
|
+
|
|
258
|
+
1. **Build + chạy như service** — `/etc/systemd/system/webcake-mcp.service`:
|
|
259
|
+
```ini
|
|
260
|
+
[Service]
|
|
261
|
+
WorkingDirectory=/opt/webcake-landing-mcp
|
|
262
|
+
ExecStart=/usr/bin/node dist/index.js serve --port 8787
|
|
263
|
+
Environment=WEBCAKE_API_BASE=https://api.webcake.io
|
|
264
|
+
Environment=WEBCAKE_JWT=<ljwt> # chỉ cho single-account — xem ghi chú auth dưới
|
|
265
|
+
Restart=always
|
|
266
|
+
[Install]
|
|
267
|
+
WantedBy=multi-user.target
|
|
268
|
+
```
|
|
269
|
+
`sudo systemctl enable --now webcake-mcp` (build 1 lần: `npm install && npm run build`).
|
|
270
|
+
2. **HTTPS + domain** (claude.ai bắt buộc https) — vd Caddy tự cấp TLS, `/etc/caddy/Caddyfile`:
|
|
271
|
+
```
|
|
272
|
+
mcp.yourdomain.com { reverse_proxy localhost:8787 }
|
|
273
|
+
```
|
|
274
|
+
3. **Thêm vào claude.ai** → Remote MCP server URL = `https://mcp.yourdomain.com/mcp`.
|
|
275
|
+
|
|
276
|
+
**Auth trên server chia sẻ:**
|
|
277
|
+
- **Single-account** (dùng được với dialog ngay): đặt `WEBCAKE_JWT` ở env service → mọi người dùng connector
|
|
278
|
+
chung 1 tài khoản Webcake. Giữ URL riêng tư / có cổng chặn; token hết hạn (~90 ngày).
|
|
279
|
+
- **Per-user** (mỗi người 1 account): cho mỗi người một URL với `?jwt=<ljwt>` riêng (chạy được qua dialog,
|
|
280
|
+
nhưng token lộ trong log), hoặc dùng client hỗ trợ header (`mcp-remote --header …`), hoặc thêm **OAuth**
|
|
281
|
+
(chưa làm) cho gọn nhất.
|
|
282
|
+
|
|
187
283
|
## Cài thủ công (local)
|
|
188
284
|
|
|
189
285
|
```bash
|
|
@@ -197,15 +293,67 @@ npm run smoke # self-test offline của factory + validator (in "ALL GOOD")
|
|
|
197
293
|
Các tool tham chiếu/kiểm tra chạy với **zero config**. Biến môi trường chỉ cần cho các tool lưu trữ
|
|
198
294
|
(`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizations`).
|
|
199
295
|
|
|
296
|
+
## Kết nối một lần — tự lấy token (`login`)
|
|
297
|
+
|
|
298
|
+
Thay vì copy JWT bằng tay, chạy:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Production — zero config (mặc định: connect qua webcake.io, API qua api.webcake.io):
|
|
302
|
+
npx -y webcake-landing-mcp login
|
|
303
|
+
|
|
304
|
+
# Local dev — trỏ vào SPA (5173) + API (5800) ở máy:
|
|
305
|
+
node dist/index.js login \
|
|
306
|
+
--connect-url http://localhost:5173/mcp-connect \
|
|
307
|
+
--api-base http://localhost:5800
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Nó mở browser → (đăng nhập Webcake nếu cần) → token được lưu vào
|
|
311
|
+
`~/.webcake-landing-mcp/auth.json`, server tự đọc.
|
|
312
|
+
|
|
313
|
+
Bạn đang đăng nhập Webcake sẵn trong browser, nên `login` chỉ mở trang "connect" của Webcake — trang này
|
|
314
|
+
đọc cookie **`ljwt`** (landing) và trả token về một callback loopback nội bộ — khỏi copy-paste. Token đã lưu
|
|
315
|
+
được dùng bởi **cả** server stdio lẫn deploy `serve` một-người-dùng (env vẫn ưu tiên). Landing JWT sống ~90
|
|
316
|
+
ngày nên hiếm khi phải kết nối lại.
|
|
317
|
+
|
|
318
|
+
Hai URL, đừng nhầm:
|
|
319
|
+
|
|
320
|
+
- **Trang connect = SPA** (`--connect-url` / `WEBCAKE_CONNECT_URL`): `https://webcake.io/mcp-connect` ở prod,
|
|
321
|
+
`http://localhost:5173/mcp-connect` ở local. Nếu không, suy ra từ `WEBCAKE_APP_BASE` + `/mcp-connect`,
|
|
322
|
+
mặc định `https://webcake.io/mcp-connect`.
|
|
323
|
+
- **API base = backend** (`--api-base` / `WEBCAKE_API_BASE`): `https://api.webcake.io` ở prod,
|
|
324
|
+
`http://localhost:5800` ở local. Mặc định `https://api.webcake.io`.
|
|
325
|
+
|
|
326
|
+
Cờ khác: `--org-id`, `--port`, `--no-open`. Thư mục file lưu: `WEBCAKE_CONFIG_DIR` (mặc định
|
|
327
|
+
`~/.webcake-landing-mcp`).
|
|
328
|
+
|
|
329
|
+
**Endpoint cần thêm ở backend** (trong Webcake backend — nơi giữ cookie session):
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
GET /mcp-connect?redirect_uri=<loopback>&state=<s>
|
|
333
|
+
→ đọc cookie `ljwt` (landing token của user đang đăng nhập)
|
|
334
|
+
→ 302 tới <redirect_uri>?token=<ljwt>&state=<s>
|
|
335
|
+
(nếu chưa có cookie: 302 sang trang login trước, xong quay lại đây)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Để an toàn, chỉ chấp nhận `redirect_uri` ở `http://127.0.0.1:*` / `http://localhost:*`.
|
|
339
|
+
(Mẫu tham khảo: `builderx_spa/src/views/McpConnect.vue` đọc `cookies.get('ljwt')` — nên flow này làm hẳn ở
|
|
340
|
+
SPA cũng được, khỏi cần route backend.)
|
|
341
|
+
|
|
342
|
+
> Remote đa người dùng (dialog claude.ai) không làm được browser loopback này — ở đó mỗi user gửi token riêng
|
|
343
|
+
> qua header `x-webcake-jwt` (xem mục remote-connector ở trên).
|
|
344
|
+
|
|
200
345
|
## Biến môi trường
|
|
201
346
|
|
|
202
347
|
| Biến | Bắt buộc | Mô tả |
|
|
203
348
|
|----------|----------|-------------|
|
|
204
|
-
| `
|
|
349
|
+
| `WEBCAKE_ENV` | Không | Môi trường có tên: `local` \| `staging` \| `prod`. Điền sẵn `WEBCAKE_API_BASE` + `WEBCAKE_APP_BASE` từ preset (xem bảng bên dưới). Cũng đặt được qua cờ `--env <name>`. Biến tường minh sẽ thắng. |
|
|
350
|
+
| `WEBCAKE_API_BASE` | Không* | Base URL backend, ví dụ `http://localhost:5800`. Cần để lưu trang (hoặc đặt `WEBCAKE_ENV`). |
|
|
205
351
|
| `WEBCAKE_JWT` | Không* | JWT tài khoản (auth dashboard). Cần để lưu trang — sẽ hết hạn, làm mới khi cần. |
|
|
206
352
|
| `WEBCAKE_ORG_ID` | Không | Organization mặc định cho `create_page` (bị ghi đè bởi tham số `organization_id`). Bỏ trống → trang cá nhân. |
|
|
207
353
|
| `WEBCAKE_HOST` | Không | Header `Host` tuỳ chọn (Phoenix route theo host, ví dụ `builder.localhost`). |
|
|
208
354
|
| `WEBCAKE_APP_BASE` | Không | Base tuỳ chọn để dựng URL editor/preview trong kết quả. |
|
|
355
|
+
| `WEBCAKE_CONNECT_URL` | Không | Trang "connect" (SPA) cho `login` (mặc định `https://webcake.io/mcp-connect`; nếu không thì `WEBCAKE_APP_BASE` + `/mcp-connect`). |
|
|
356
|
+
| `WEBCAKE_CONFIG_DIR` | Không | Thư mục chứa `auth.json` do `login` ghi (mặc định `~/.webcake-landing-mcp`). |
|
|
209
357
|
|
|
210
358
|
> \* `WEBCAKE_API_BASE` và `WEBCAKE_JWT` chỉ cần cho các tool lưu trữ. Các tool tham chiếu và kiểm tra
|
|
211
359
|
> (`get_generation_guide`, `list_elements`, `get_element`, `validate_page`, …) chạy không cần chúng.
|
|
@@ -213,6 +361,28 @@ Các tool tham chiếu/kiểm tra chạy với **zero config**. Biến môi trư
|
|
|
213
361
|
> Lưu trang sẽ ghi một trang thật vào nơi `WEBCAKE_API_BASE` trỏ tới, dùng JWT làm tài khoản đó.
|
|
214
362
|
> Hãy bắt đầu với local/staging.
|
|
215
363
|
|
|
364
|
+
### Môi trường (`--env` / `WEBCAKE_ENV`)
|
|
365
|
+
|
|
366
|
+
Thay vì đặt thủ công cả hai base URL, hãy chọn một môi trường có tên — một nguồn sự thật duy nhất
|
|
367
|
+
cho API + app base (mặc định là `prod`):
|
|
368
|
+
|
|
369
|
+
| `--env` / `WEBCAKE_ENV` | API base (`WEBCAKE_API_BASE`) | App base (`WEBCAKE_APP_BASE`) |
|
|
370
|
+
|-------------------------|-------------------------------|-------------------------------|
|
|
371
|
+
| `local` | `http://localhost:5800` | `http://localhost:5173` |
|
|
372
|
+
| `staging` | `https://api.staging.webcake.io` | `https://staging.webcake.io` |
|
|
373
|
+
| `prod` *(mặc định)* | `https://api.webcake.io` | `https://webcake.io` |
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
node dist/index.js serve --env staging # server remote trỏ backend staging
|
|
377
|
+
node dist/index.js login --env local # đăng nhập vào SPA + API local
|
|
378
|
+
WEBCAKE_ENV=prod node dist/index.js # stdio, prod (dạng biến môi trường)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`WEBCAKE_API_BASE` / `WEBCAKE_APP_BASE` (hoặc `--api-base`) tường minh vẫn ghi đè preset theo từng
|
|
382
|
+
trường. Trên server HTTP remote, client có thể ghi đè môi trường của server theo từng request bằng
|
|
383
|
+
header **`x-webcake-env`** hoặc query **`?env=`** (ví dụ `…/mcp?jwt=<token>&env=staging`) — nên một
|
|
384
|
+
server phục vụ được nhiều môi trường.
|
|
385
|
+
|
|
216
386
|
### Cách lấy `WEBCAKE_JWT`
|
|
217
387
|
|
|
218
388
|
1. Mở dashboard builder WebCake và đăng nhập
|
package/dist/auth/login.js
CHANGED
|
@@ -19,13 +19,7 @@
|
|
|
19
19
|
import { createServer } from "node:http";
|
|
20
20
|
import { randomBytes } from "node:crypto";
|
|
21
21
|
import { spawn } from "node:child_process";
|
|
22
|
-
import { saveSavedConfig } from "../persistence/config.js";
|
|
23
|
-
// Production defaults — the connect page lives on the SPA (webcake.io), the API
|
|
24
|
-
// lives on api.webcake.io. For local dev override with --connect-url / --api-base
|
|
25
|
-
// (e.g. http://localhost:5173/mcp-connect and http://localhost:5800) or the
|
|
26
|
-
// WEBCAKE_APP_BASE / WEBCAKE_API_BASE env vars.
|
|
27
|
-
const DEFAULT_CONNECT_URL = "https://webcake.io/mcp-connect";
|
|
28
|
-
const DEFAULT_API_BASE = "https://api.webcake.io";
|
|
22
|
+
import { saveSavedConfig, resolveEnv, ENVIRONMENTS } from "../persistence/config.js";
|
|
29
23
|
function parseArgs(argv) {
|
|
30
24
|
const get = (name) => {
|
|
31
25
|
const i = argv.indexOf(name);
|
|
@@ -53,21 +47,22 @@ function openBrowser(url) {
|
|
|
53
47
|
const SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>Connected</title>
|
|
54
48
|
<body style="font-family:system-ui;text-align:center;padding:48px">
|
|
55
49
|
<h2>✓ Connected to Webcake</h2><p>You can close this tab and return to your terminal.</p></body>`;
|
|
56
|
-
function resolveConnectUrl(opts) {
|
|
50
|
+
function resolveConnectUrl(opts, appBase) {
|
|
57
51
|
if (opts.connectUrl)
|
|
58
52
|
return opts.connectUrl;
|
|
59
53
|
if (process.env.WEBCAKE_CONNECT_URL)
|
|
60
54
|
return process.env.WEBCAKE_CONNECT_URL;
|
|
61
|
-
// The connect page is on the SPA (
|
|
62
|
-
|
|
63
|
-
if (appBase)
|
|
64
|
-
return `${appBase.replace(/\/+$/, "")}/mcp-connect`;
|
|
65
|
-
return DEFAULT_CONNECT_URL;
|
|
55
|
+
// The connect page is on the SPA (appBase, from the env preset), NOT the API base.
|
|
56
|
+
return `${appBase.replace(/\/+$/, "")}/mcp-connect`;
|
|
66
57
|
}
|
|
67
58
|
export async function runLogin(argv) {
|
|
68
59
|
const opts = parseArgs(argv);
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
// Named environment (set by the global --env flag / WEBCAKE_ENV); prod is the
|
|
61
|
+
// zero-config default. Explicit --api-base / WEBCAKE_APP_BASE still win per field.
|
|
62
|
+
const preset = resolveEnv(process.env.WEBCAKE_ENV) ?? ENVIRONMENTS.prod;
|
|
63
|
+
const apiBase = opts.apiBase || process.env.WEBCAKE_API_BASE || preset.apiBase;
|
|
64
|
+
const appBase = process.env.WEBCAKE_APP_BASE || preset.appBase;
|
|
65
|
+
const connectUrl = resolveConnectUrl(opts, appBase);
|
|
71
66
|
const state = randomBytes(16).toString("hex");
|
|
72
67
|
await new Promise((resolve, reject) => {
|
|
73
68
|
const server = createServer((req, res) => {
|
|
@@ -83,15 +78,13 @@ export async function runLogin(argv) {
|
|
|
83
78
|
}
|
|
84
79
|
const path = saveSavedConfig({
|
|
85
80
|
jwt: token,
|
|
86
|
-
|
|
81
|
+
base: apiBase.replace(/\/+$/, ""),
|
|
82
|
+
appBase: appBase.replace(/\/+$/, ""),
|
|
87
83
|
...(opts.orgId ? { orgId: opts.orgId } : {}),
|
|
88
84
|
savedAt: new Date().toISOString(),
|
|
89
85
|
});
|
|
90
86
|
res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
|
|
91
|
-
console.error(`\n✓ Connected. Token saved to ${path}
|
|
92
|
-
if (!apiBase) {
|
|
93
|
-
console.error(" tip: also set WEBCAKE_API_BASE (or pass --api-base) so the server knows the backend URL.");
|
|
94
|
-
}
|
|
87
|
+
console.error(`\n✓ Connected. Token saved to ${path} (api ${apiBase}).`);
|
|
95
88
|
server.close();
|
|
96
89
|
resolve();
|
|
97
90
|
});
|
package/dist/http.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Stateful sessions: an `initialize` POST (no session id) spins up a fresh
|
|
6
6
|
* McpServer + transport and returns an `mcp-session-id`; later requests reuse it
|
|
7
|
-
* via that header.
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
7
|
+
* via that header. Each request carries the caller's OWN Webcake JWT — via a header
|
|
8
|
+
* (x-webcake-jwt / Authorization) OR a URL query param (.../mcp?jwt=<token>, for
|
|
9
|
+
* clients like the claude.ai dialog that can't set headers; see applyQueryAuth +
|
|
10
|
+
* persistence/config.ts#configFromHeaders). So a hosted server is multi-user.
|
|
10
11
|
*
|
|
11
12
|
* All logging stays on stderr (console.error), same as stdio mode.
|
|
12
13
|
*/
|
|
@@ -30,6 +31,36 @@ async function readBody(req) {
|
|
|
30
31
|
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
31
32
|
return raw ? JSON.parse(raw) : undefined;
|
|
32
33
|
}
|
|
34
|
+
// Map credentials passed in the URL query (e.g. .../mcp?jwt=<token>) onto the
|
|
35
|
+
// x-webcake-* headers so the normal per-request config path handles them. This is
|
|
36
|
+
// for clients that can't set custom headers — notably the claude.ai connector
|
|
37
|
+
// dialog, which only takes a URL. An explicit header always wins over the query.
|
|
38
|
+
// SECURITY: a token in the URL can land in access/proxy logs — prefer headers,
|
|
39
|
+
// require HTTPS, and disable query-string logging on your reverse proxy.
|
|
40
|
+
const QUERY_AUTH = {
|
|
41
|
+
jwt: "x-webcake-jwt",
|
|
42
|
+
env: "x-webcake-env",
|
|
43
|
+
api_base: "x-webcake-api-base",
|
|
44
|
+
org_id: "x-webcake-org-id",
|
|
45
|
+
host: "x-webcake-host",
|
|
46
|
+
app_base: "x-webcake-app-base",
|
|
47
|
+
};
|
|
48
|
+
function applyQueryAuth(req) {
|
|
49
|
+
const q = (req.url ?? "").indexOf("?");
|
|
50
|
+
if (q === -1)
|
|
51
|
+
return;
|
|
52
|
+
const params = new URLSearchParams((req.url ?? "").slice(q + 1));
|
|
53
|
+
for (const [param, header] of Object.entries(QUERY_AUTH)) {
|
|
54
|
+
const value = params.get(param);
|
|
55
|
+
// Only fill in when there's no explicit header (header wins). The transport
|
|
56
|
+
// builds its Request from `req.rawHeaders` (via @hono/node-server), so we MUST
|
|
57
|
+
// push there — mutating `req.headers` alone is not seen by the tool handlers.
|
|
58
|
+
if (value && req.headers[header] == null) {
|
|
59
|
+
req.headers[header] = value;
|
|
60
|
+
req.rawHeaders.push(header, value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
33
64
|
export async function startHttpServer(port) {
|
|
34
65
|
// mcp-session-id -> live transport (each bound to its own McpServer instance).
|
|
35
66
|
const transports = new Map();
|
|
@@ -41,6 +72,8 @@ export async function startHttpServer(port) {
|
|
|
41
72
|
}
|
|
42
73
|
if (path !== MCP_PATH)
|
|
43
74
|
return rpcError(res, 404, `Not found. Send MCP requests to ${MCP_PATH}.`);
|
|
75
|
+
// Accept credentials via ?jwt=/?api_base=/... (for clients that can't set headers).
|
|
76
|
+
applyQueryAuth(req);
|
|
44
77
|
const sidHeader = req.headers["mcp-session-id"];
|
|
45
78
|
const sessionId = Array.isArray(sidHeader) ? sidHeader[0] : sidHeader;
|
|
46
79
|
try {
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,40 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
19
|
import { createServer } from "./server.js";
|
|
20
|
+
import { ENVIRONMENTS, ENV_NAMES, isEnvName } from "./persistence/config.js";
|
|
21
|
+
/**
|
|
22
|
+
* Global `--env <local|staging|prod>` flag (or `--env=<name>`): selects the API +
|
|
23
|
+
* app base URLs from a named preset by setting WEBCAKE_ENV, which readConfig + login
|
|
24
|
+
* then pick up. Explicit WEBCAKE_API_BASE / WEBCAKE_APP_BASE still win. An unknown
|
|
25
|
+
* value from the flag fails fast; an unknown WEBCAKE_ENV is dropped so explicit
|
|
26
|
+
* bases (or per-request headers) still resolve. stderr only — stdout is the MCP channel.
|
|
27
|
+
*/
|
|
28
|
+
function applyEnvFlag(argv) {
|
|
29
|
+
let fromFlag;
|
|
30
|
+
for (let i = 2; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a === "--env")
|
|
33
|
+
fromFlag = argv[i + 1];
|
|
34
|
+
else if (a.startsWith("--env="))
|
|
35
|
+
fromFlag = a.slice("--env=".length);
|
|
36
|
+
}
|
|
37
|
+
const name = fromFlag ?? process.env.WEBCAKE_ENV;
|
|
38
|
+
if (!name)
|
|
39
|
+
return;
|
|
40
|
+
if (!isEnvName(name)) {
|
|
41
|
+
console.error(`[webcake] unknown environment "${name}". Valid: ${ENV_NAMES.join(", ")}.`);
|
|
42
|
+
if (fromFlag)
|
|
43
|
+
process.exit(1); // explicit flag typo → fail fast
|
|
44
|
+
delete process.env.WEBCAKE_ENV; // bad WEBCAKE_ENV → ignore, fall through to explicit bases
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
process.env.WEBCAKE_ENV = name;
|
|
48
|
+
const p = ENVIRONMENTS[name];
|
|
49
|
+
console.error(`[webcake] environment "${name}" — api ${p.apiBase}, app ${p.appBase}`);
|
|
50
|
+
}
|
|
20
51
|
async function main() {
|
|
52
|
+
// Resolve the named environment (--env / WEBCAKE_ENV) before any config is read.
|
|
53
|
+
applyEnvFlag(process.argv);
|
|
21
54
|
// Subcommand dispatch: `webcake-landing-mcp install|uninstall` runs the
|
|
22
55
|
// bundled IDE installer instead of starting the MCP server. Default (no
|
|
23
56
|
// subcommand) starts the stdio server as usual.
|
package/dist/install.js
CHANGED
|
@@ -22,6 +22,8 @@ import { dirname, join } from "node:path";
|
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { spawnSync } from "node:child_process";
|
|
24
24
|
import { createInterface } from "node:readline";
|
|
25
|
+
import { ENVIRONMENTS, isEnvName } from "./persistence/config.js";
|
|
26
|
+
import { runLogin } from "./auth/login.js";
|
|
25
27
|
const NAME = "webcake-landing";
|
|
26
28
|
const PKG = "webcake-landing-mcp";
|
|
27
29
|
const HOME = homedir();
|
|
@@ -290,18 +292,19 @@ function printHelp() {
|
|
|
290
292
|
${c.bold}webcake-landing-mcp install${c.reset} — configure the MCP server in your IDE(s)
|
|
291
293
|
|
|
292
294
|
${c.bold}Usage${c.reset}
|
|
293
|
-
npx -y ${PKG} install # interactive (
|
|
294
|
-
npx -y ${PKG} install --ide all # non-interactive, all IDEs
|
|
295
|
-
npx -y ${PKG} install --ide claude-code --jwt <JWT>
|
|
295
|
+
npx -y ${PKG} install # interactive: pick environment, log in (or paste a JWT), pick IDEs
|
|
296
|
+
npx -y ${PKG} install --ide all # non-interactive, all IDEs (defaults to --env prod)
|
|
297
|
+
npx -y ${PKG} install --ide claude-code --env local --jwt <JWT>
|
|
296
298
|
npx -y ${PKG} uninstall # remove from every IDE config
|
|
297
299
|
|
|
298
300
|
${c.bold}Flags${c.reset}
|
|
299
301
|
--ide <list> comma list: claude-desktop, claude-code, cursor, windsurf, augment, codex, all
|
|
300
|
-
--
|
|
301
|
-
--jwt <token> WEBCAKE_JWT (account token; optional
|
|
302
|
-
--org-id <id> WEBCAKE_ORG_ID (optional)
|
|
303
|
-
--
|
|
304
|
-
--app-base <url>
|
|
302
|
+
--env <name> WEBCAKE_ENV: local | staging | prod (default prod) — sets the API + app base URLs
|
|
303
|
+
--jwt <token> WEBCAKE_JWT (account token; optional — or log in via the browser interactively)
|
|
304
|
+
--org-id <id> WEBCAKE_ORG_ID (optional default organization)
|
|
305
|
+
--api-base <url> override the --env API base (advanced)
|
|
306
|
+
--app-base <url> override the --env app base (advanced)
|
|
307
|
+
--host <host> WEBCAKE_HOST (advanced; Phoenix host-routing header)
|
|
305
308
|
--npx | --local force the launch command form (default: auto-detect)
|
|
306
309
|
-y, --yes accept defaults, skip confirmations
|
|
307
310
|
--uninstall remove the server from all IDE configs
|
|
@@ -317,36 +320,72 @@ export async function runInstaller(argv) {
|
|
|
317
320
|
log(`\n${c.cyan}${c.bold}Webcake Landing MCP — installer${c.reset}`);
|
|
318
321
|
log(`${c.gray}Build & edit Webcake landing pages from a prompt. 12 tools.${c.reset}`);
|
|
319
322
|
const interactive = !o.ide && process.stdin.isTTY && process.stdout.isTTY;
|
|
320
|
-
// 1)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
let
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
log(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
323
|
+
// 1) environment — one choice sets the API + app base URLs (replaces the old
|
|
324
|
+
// separate WEBCAKE_API_BASE / WEBCAKE_APP_BASE prompts). The global --env flag
|
|
325
|
+
// / WEBCAKE_ENV is already validated in index.ts (applyEnvFlag).
|
|
326
|
+
let envName = isEnvName(process.env.WEBCAKE_ENV) ? process.env.WEBCAKE_ENV : "prod";
|
|
327
|
+
if (interactive && !isEnvName(process.env.WEBCAKE_ENV)) {
|
|
328
|
+
log(`\n${c.bold}1) Environment${c.reset} ${c.gray}(sets the Webcake API + app URLs)${c.reset}`);
|
|
329
|
+
log(` 1) prod ${c.gray}${ENVIRONMENTS.prod.apiBase}${c.reset} ${c.gray}(default)${c.reset}`);
|
|
330
|
+
log(` 2) staging ${c.gray}${ENVIRONMENTS.staging.apiBase}${c.reset}`);
|
|
331
|
+
log(` 3) local ${c.gray}${ENVIRONMENTS.local.apiBase}${c.reset}`);
|
|
332
|
+
const pick = (await ask(" Select [1=prod, Enter to accept]: ")).trim();
|
|
333
|
+
envName = { "1": "prod", "2": "staging", "3": "local" }[pick] ?? "prod";
|
|
334
|
+
}
|
|
335
|
+
process.env.WEBCAKE_ENV = envName; // so `login` below connects to the same environment
|
|
336
|
+
const preset = ENVIRONMENTS[envName];
|
|
337
|
+
// 2) authentication — interactively ASK how to authenticate: browser login (token
|
|
338
|
+
// saved to ~/.webcake-landing-mcp/auth.json) or a pasted JWT. An explicit --jwt
|
|
339
|
+
// skips the prompt; a non-TTY falls back to the WEBCAKE_JWT env var (for scripted
|
|
340
|
+
// installs). Reference/validation tools work with no auth at all.
|
|
341
|
+
let jwt = o.jwt ?? "";
|
|
342
|
+
let authNote = jwt ? "JWT (from --jwt)" : "";
|
|
343
|
+
if (interactive && !jwt) {
|
|
344
|
+
log(`\n${c.bold}2) Authentication${c.reset} ${c.gray}(only needed to save pages to your account)${c.reset}`);
|
|
345
|
+
log(` 1) Log in via browser ${c.gray}(recommended — opens Webcake, saves a token)${c.reset}`);
|
|
346
|
+
log(` 2) Paste a JWT token`);
|
|
347
|
+
log(` 3) Skip for now ${c.gray}(reference/validation tools still work)${c.reset}`);
|
|
348
|
+
const pick = (await ask(" Select [1]: ")).trim() || "1";
|
|
349
|
+
if (pick === "1") {
|
|
350
|
+
info(`Connecting to ${preset.appBase} …`);
|
|
351
|
+
try {
|
|
352
|
+
await runLogin([]); // reads WEBCAKE_ENV for the connect URL + API base; saves auth.json
|
|
353
|
+
authNote = "browser login (saved to auth.json)";
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
warn(`Login didn't complete (${e?.message ?? e}). Paste a JWT now, or run \`${PKG} login\` later.`);
|
|
357
|
+
jwt = (await ask(" WEBCAKE_JWT (or Enter to skip): ")).trim();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (pick === "2") {
|
|
361
|
+
jwt = (await ask(" WEBCAKE_JWT: ")).trim();
|
|
362
|
+
}
|
|
335
363
|
}
|
|
336
|
-
else if (!
|
|
337
|
-
|
|
364
|
+
else if (!interactive) {
|
|
365
|
+
jwt = jwt || process.env.WEBCAKE_JWT || ""; // scripted / CI: fall back to ambient env
|
|
366
|
+
}
|
|
367
|
+
if (!authNote)
|
|
368
|
+
authNote = jwt ? "JWT" : "none — reference tools only";
|
|
369
|
+
// 3) optional default organization for create_page
|
|
370
|
+
let orgId = o.orgId ?? process.env.WEBCAKE_ORG_ID ?? "";
|
|
371
|
+
if (interactive && !orgId) {
|
|
372
|
+
orgId = (await ask(`\n${c.bold}3) WEBCAKE_ORG_ID${c.reset} ${c.gray}(optional, Enter to skip): ${c.reset}`)).trim();
|
|
338
373
|
}
|
|
339
|
-
|
|
340
|
-
|
|
374
|
+
// env block written into IDE configs: WEBCAKE_ENV drives the URLs. A pasted JWT is
|
|
375
|
+
// written too; a browser login lives in auth.json instead. --api-base/--app-base/
|
|
376
|
+
// --host stay as advanced overrides for non-standard setups.
|
|
377
|
+
const env = { WEBCAKE_ENV: envName };
|
|
341
378
|
if (jwt)
|
|
342
379
|
env.WEBCAKE_JWT = jwt;
|
|
343
380
|
if (orgId)
|
|
344
381
|
env.WEBCAKE_ORG_ID = orgId;
|
|
345
|
-
if (
|
|
346
|
-
env.
|
|
347
|
-
if (appBase)
|
|
348
|
-
env.WEBCAKE_APP_BASE = appBase;
|
|
349
|
-
|
|
382
|
+
if (o.apiBase)
|
|
383
|
+
env.WEBCAKE_API_BASE = o.apiBase;
|
|
384
|
+
if (o.appBase)
|
|
385
|
+
env.WEBCAKE_APP_BASE = o.appBase;
|
|
386
|
+
if (o.host)
|
|
387
|
+
env.WEBCAKE_HOST = o.host;
|
|
388
|
+
// 4) which IDEs
|
|
350
389
|
let ides = [];
|
|
351
390
|
if (o.ide) {
|
|
352
391
|
ides = o.ide
|
|
@@ -355,7 +394,7 @@ export async function runInstaller(argv) {
|
|
|
355
394
|
.filter(Boolean);
|
|
356
395
|
}
|
|
357
396
|
else if (interactive) {
|
|
358
|
-
log(`\n${c.bold}
|
|
397
|
+
log(`\n${c.bold}4) Which IDE(s) to configure?${c.reset}`);
|
|
359
398
|
log(" 1) Claude Desktop 2) Claude Code (CLI) 3) Cursor");
|
|
360
399
|
log(" 4) Windsurf 5) Augment (VS Code) 6) Codex");
|
|
361
400
|
log(" 7) All 0) Skip");
|
|
@@ -383,13 +422,13 @@ export async function runInstaller(argv) {
|
|
|
383
422
|
warn("No IDE selected — skipping configuration.");
|
|
384
423
|
return;
|
|
385
424
|
}
|
|
386
|
-
//
|
|
425
|
+
// 5) write
|
|
387
426
|
const launch = resolveLaunch(o);
|
|
388
|
-
log(`\n${c.bold}
|
|
427
|
+
log(`\n${c.bold}5) Writing config${c.reset} ${c.gray}(launch: ${launch.command} ${launch.args.join(" ")})${c.reset}`);
|
|
389
428
|
runConfigure(ides, launch, env);
|
|
390
|
-
//
|
|
429
|
+
// 6) summary
|
|
391
430
|
log(`\n${c.green}${c.bold}✓ Done.${c.reset}`);
|
|
392
|
-
log(` ${c.gray}
|
|
393
|
-
log(` ${c.gray}
|
|
431
|
+
log(` ${c.gray}Environment : ${envName} (api ${o.apiBase || preset.apiBase})${c.reset}`);
|
|
432
|
+
log(` ${c.gray}Auth : ${authNote}${c.reset}`);
|
|
394
433
|
log(` Restart your IDE, then ask the AI: “Build a Webcake landing page”.\n`);
|
|
395
434
|
}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* { config: null, missing } when required values are absent so the persistence
|
|
13
13
|
* tools can report exactly what to provide.
|
|
14
14
|
*
|
|
15
|
+
* WEBCAKE_ENV optional named environment (local|staging|prod) — fills in the
|
|
16
|
+
* API + app base URLs from a preset (see ENVIRONMENTS below). An
|
|
17
|
+
* explicit WEBCAKE_API_BASE / WEBCAKE_APP_BASE still wins over it.
|
|
15
18
|
* WEBCAKE_API_BASE e.g. http://localhost:5800 (required to call the backend)
|
|
16
19
|
* WEBCAKE_JWT the account JWT (required to call the backend)
|
|
17
20
|
* WEBCAKE_ORG_ID optional default organization id for create_page
|
|
@@ -22,9 +25,32 @@
|
|
|
22
25
|
import { homedir } from "node:os";
|
|
23
26
|
import { join } from "node:path";
|
|
24
27
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
28
|
+
/**
|
|
29
|
+
* Named deployment environments — the single source of truth for the API + app
|
|
30
|
+
* base URLs. Selecting one (via the `--env` flag, WEBCAKE_ENV, the `x-webcake-env`
|
|
31
|
+
* header, or `?env=` in the URL) fills in both bases so callers don't repeat them.
|
|
32
|
+
* Explicit WEBCAKE_API_BASE / WEBCAKE_APP_BASE (or per-request overrides) win over
|
|
33
|
+
* the preset. `apiBase` is the backend; `appBase` is the SPA (editor/preview/connect).
|
|
34
|
+
*/
|
|
35
|
+
export const ENVIRONMENTS = {
|
|
36
|
+
local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173" },
|
|
37
|
+
staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io" },
|
|
38
|
+
prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io" },
|
|
39
|
+
};
|
|
40
|
+
export const ENV_NAMES = Object.keys(ENVIRONMENTS);
|
|
41
|
+
/** True when `v` names a known environment (local|staging|prod). */
|
|
42
|
+
export function isEnvName(v) {
|
|
43
|
+
return typeof v === "string" && Object.prototype.hasOwnProperty.call(ENVIRONMENTS, v);
|
|
44
|
+
}
|
|
45
|
+
/** The base URLs for a named environment, or undefined when the name is absent/unknown. */
|
|
46
|
+
export function resolveEnv(name) {
|
|
47
|
+
return isEnvName(name) ? ENVIRONMENTS[name] : undefined;
|
|
48
|
+
}
|
|
25
49
|
export function readConfig(overrides = {}) {
|
|
26
50
|
const saved = readSavedConfig();
|
|
27
|
-
|
|
51
|
+
// A named environment supplies default base URLs; explicit values still win.
|
|
52
|
+
const preset = resolveEnv(overrides.env ?? process.env.WEBCAKE_ENV);
|
|
53
|
+
const base = overrides.base ?? process.env.WEBCAKE_API_BASE ?? preset?.apiBase ?? saved.base;
|
|
28
54
|
const jwt = overrides.jwt ?? process.env.WEBCAKE_JWT ?? saved.jwt;
|
|
29
55
|
const missing = [];
|
|
30
56
|
if (!base)
|
|
@@ -39,7 +65,7 @@ export function readConfig(overrides = {}) {
|
|
|
39
65
|
jwt: jwt,
|
|
40
66
|
orgId: overrides.orgId ?? process.env.WEBCAKE_ORG_ID ?? saved.orgId,
|
|
41
67
|
host: overrides.host ?? process.env.WEBCAKE_HOST ?? saved.host,
|
|
42
|
-
appBase: (overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? saved.appBase)?.replace(/\/+$/, ""),
|
|
68
|
+
appBase: (overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? preset?.appBase ?? saved.appBase)?.replace(/\/+$/, ""),
|
|
43
69
|
},
|
|
44
70
|
missing: [],
|
|
45
71
|
};
|
|
@@ -53,9 +79,10 @@ function header(headers, name) {
|
|
|
53
79
|
* send its own credentials per request instead of a server-wide env token:
|
|
54
80
|
* x-webcake-jwt the account JWT (or `Authorization: Bearer <jwt>`)
|
|
55
81
|
* x-webcake-org-id organization id
|
|
56
|
-
* x-webcake-
|
|
82
|
+
* x-webcake-env named environment (local|staging|prod) for the base URLs
|
|
83
|
+
* x-webcake-api-base backend base URL (overrides the env preset)
|
|
57
84
|
* x-webcake-host Host header override
|
|
58
|
-
* x-webcake-app-base editor/preview URL base
|
|
85
|
+
* x-webcake-app-base editor/preview URL base (overrides the env preset)
|
|
59
86
|
* Any header that is absent falls back to the corresponding env var in readConfig.
|
|
60
87
|
*/
|
|
61
88
|
export function configFromHeaders(headers) {
|
|
@@ -67,6 +94,7 @@ export function configFromHeaders(headers) {
|
|
|
67
94
|
orgId: header(headers, "x-webcake-org-id"),
|
|
68
95
|
host: header(headers, "x-webcake-host"),
|
|
69
96
|
appBase: header(headers, "x-webcake-app-base"),
|
|
97
|
+
env: header(headers, "x-webcake-env"),
|
|
70
98
|
};
|
|
71
99
|
}
|
|
72
100
|
/** Directory for the saved auth file (override with WEBCAKE_CONFIG_DIR). */
|
package/dist/smoke.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createElement, CONTAINER_TYPES, FIELD_TYPES, LIBRARY, ELEMENT_TYPES, ELEMENTS, } from "./domains/landing/elements/index.js";
|
|
6
6
|
import { validatePage, pageSchema } from "./domains/landing/validate.js";
|
|
7
|
+
import { readConfig, resolveEnv, ENV_NAMES } from "./persistence/config.js";
|
|
7
8
|
let failures = 0;
|
|
8
9
|
const check = (name, cond, extra) => {
|
|
9
10
|
if (cond) {
|
|
@@ -208,5 +209,21 @@ const bindingsGood = {
|
|
|
208
209
|
};
|
|
209
210
|
const rbg = validatePage(bindingsGood);
|
|
210
211
|
check("clean form has no binding warnings", rbg.warnings.length === 0, rbg.warnings);
|
|
212
|
+
console.log("== config: named environment presets (local/staging/prod) ==");
|
|
213
|
+
{
|
|
214
|
+
// Deterministic: isolate from any ambient WEBCAKE_* and the saved auth.json on the dev box.
|
|
215
|
+
for (const k of ["WEBCAKE_API_BASE", "WEBCAKE_APP_BASE", "WEBCAKE_ENV", "WEBCAKE_JWT", "WEBCAKE_ORG_ID", "WEBCAKE_HOST"])
|
|
216
|
+
delete process.env[k];
|
|
217
|
+
process.env.WEBCAKE_CONFIG_DIR = "/nonexistent/webcake-smoke";
|
|
218
|
+
check("env names are local/staging/prod", setEq(new Set(ENV_NAMES), ["local", "staging", "prod"]), ENV_NAMES);
|
|
219
|
+
check("staging preset resolves to api+app bases", resolveEnv("staging")?.apiBase === "https://api.staging.webcake.io" && resolveEnv("staging")?.appBase === "https://staging.webcake.io");
|
|
220
|
+
check("unknown env name → undefined", resolveEnv("bogus") === undefined);
|
|
221
|
+
const prod = readConfig({ env: "prod", jwt: "t" }).config;
|
|
222
|
+
check("readConfig(env=prod) fills api+app base", prod?.base === "https://api.webcake.io" && prod?.appBase === "https://webcake.io", prod);
|
|
223
|
+
const local = readConfig({ env: "local", jwt: "t" }).config;
|
|
224
|
+
check("readConfig(env=local) fills api+app base", local?.base === "http://localhost:5800" && local?.appBase === "http://localhost:5173", local);
|
|
225
|
+
check("explicit base overrides the preset", readConfig({ env: "prod", base: "http://x:1", jwt: "t" }).config?.base === "http://x:1");
|
|
226
|
+
check("unknown env leaves base missing", readConfig({ env: "bogus", jwt: "t" }).missing.includes("WEBCAKE_API_BASE"));
|
|
227
|
+
}
|
|
211
228
|
console.log(`\n${failures === 0 ? "ALL GOOD" : failures + " FAILURE(S)"}`);
|
|
212
229
|
process.exit(failures === 0 ? 0 : 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|