webcake-landing-mcp 1.0.7 → 1.0.9
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 +154 -7
- package/README.vi.md +152 -7
- package/dist/auth/login.js +115 -0
- package/dist/http.js +118 -0
- package/dist/index.js +23 -5
- package/dist/persistence/config.js +81 -6
- package/dist/tools/persistence.js +21 -15
- 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
|
|
|
@@ -184,6 +187,99 @@ The MCP config is the same as the local one, but `command`/`args` point at `npx`
|
|
|
184
187
|
> npx caches the package after the first run, so subsequent launches are fast. Use a pinned version
|
|
185
188
|
> (`webcake-landing-mcp@1.0.0`) if you need a reproducible build.
|
|
186
189
|
|
|
190
|
+
## Run as a remote connector (Streamable HTTP)
|
|
191
|
+
|
|
192
|
+
The server also speaks the **remote MCP** (Streamable HTTP) transport, so it can be added through
|
|
193
|
+
Claude's **"Add custom connector"** dialog via a URL — not just as a local stdio server.
|
|
194
|
+
|
|
195
|
+
Start it in HTTP mode (default port `8787`, or set `PORT` / `--port`):
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx -y webcake-landing-mcp serve --port 8787
|
|
199
|
+
# → MCP endpoint at http://localhost:8787/mcp (GET / or /health returns a status JSON)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Expose it over **HTTPS** at a public URL (a reverse proxy, a tunnel like `ngrok http 8787`, or any
|
|
203
|
+
host), then in Claude → **Add custom connector**:
|
|
204
|
+
|
|
205
|
+
- **Name**: `webcake-landing`
|
|
206
|
+
- **Remote MCP server URL**: `https://<your-host>/mcp`
|
|
207
|
+
|
|
208
|
+
The dialog has no header field, so to pass a token through it, **put it in the URL**:
|
|
209
|
+
`https://<your-host>/mcp?jwt=<ljwt>` (also accepts `&api_base=…`, `&org_id=…`, `&host=…`, `&app_base=…`).
|
|
210
|
+
Give each person a URL with their own `jwt` → **per-user without OAuth**. An explicit `x-webcake-jwt` header
|
|
211
|
+
still wins over the query. ⚠️ A token in a URL can land in access/proxy logs — require **HTTPS** and disable
|
|
212
|
+
query-string logging on your reverse proxy; a header (or OAuth) is safer when the client supports it.
|
|
213
|
+
|
|
214
|
+
### Auth — per-request, multi-user (no shared token)
|
|
215
|
+
|
|
216
|
+
In stdio mode the JWT comes from env. In HTTP mode each request carries the caller's **own** credentials
|
|
217
|
+
via headers, so a hosted server is multi-user and never bakes in a shared secret:
|
|
218
|
+
|
|
219
|
+
| Header | Maps to | Notes |
|
|
220
|
+
|--------|---------|-------|
|
|
221
|
+
| `x-webcake-jwt` (or `Authorization: Bearer <jwt>`) | `WEBCAKE_JWT` | the account token — sent per request |
|
|
222
|
+
| `x-webcake-org-id` | `WEBCAKE_ORG_ID` | default org |
|
|
223
|
+
| `x-webcake-api-base` | `WEBCAKE_API_BASE` | usually set once via env on the host instead |
|
|
224
|
+
| `x-webcake-host` | `WEBCAKE_HOST` | Phoenix host-routing header |
|
|
225
|
+
| `x-webcake-app-base` | `WEBCAKE_APP_BASE` | editor/preview URL base |
|
|
226
|
+
|
|
227
|
+
Any header that is absent falls back to the corresponding env var — so you can also run it **single-user**
|
|
228
|
+
by setting `WEBCAKE_API_BASE` + `WEBCAKE_JWT` in the host's env and keeping the URL private.
|
|
229
|
+
|
|
230
|
+
> ⚠️ The reference + generation tools (`get_generation_guide`, `list_elements`, `validate_page`, …) need
|
|
231
|
+
> no secret; only the persistence tools (`create_page`, `update_page`, …) use the JWT. If a request has no
|
|
232
|
+
> JWT, those tools return `missing_env` instead of touching the network.
|
|
233
|
+
>
|
|
234
|
+
> Note: the claude.ai connector dialog has **no header field** (only OAuth, which this server does not
|
|
235
|
+
> implement yet). Two ways around it: put the token in the URL as `?jwt=<ljwt>` (above — per-user, but the
|
|
236
|
+
> token shows up in logs), or use a header-capable client (`mcp-remote --header …`, below). A token in the
|
|
237
|
+
> server's env instead gives a shared **single-account** for everyone on that URL.
|
|
238
|
+
|
|
239
|
+
### Test it locally (no public URL needed)
|
|
240
|
+
|
|
241
|
+
`localhost` can't be used in the claude.ai dialog (Anthropic fetches the URL from its own servers). To try the
|
|
242
|
+
running `serve` server on your machine:
|
|
243
|
+
|
|
244
|
+
- **MCP Inspector** (GUI — easiest): `npx @modelcontextprotocol/inspector` → Transport **Streamable HTTP** →
|
|
245
|
+
URL `http://localhost:8787/mcp` → under Headers add `x-webcake-jwt` (+ `x-webcake-api-base`) → Connect → call tools.
|
|
246
|
+
- **`mcp-remote`** (use the remote server from a stdio client like Claude Desktop, with headers):
|
|
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` (read the `mcp-session-id` response header) → `tools/list` → `tools/call`, all with
|
|
254
|
+
`Accept: application/json, text/event-stream`.
|
|
255
|
+
|
|
256
|
+
### Deploy on a VPS
|
|
257
|
+
|
|
258
|
+
1. **Build + run as a 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> # single-account only — see auth note below
|
|
265
|
+
Restart=always
|
|
266
|
+
[Install]
|
|
267
|
+
WantedBy=multi-user.target
|
|
268
|
+
```
|
|
269
|
+
`sudo systemctl enable --now webcake-mcp` (build once: `npm install && npm run build`).
|
|
270
|
+
2. **HTTPS + domain** (claude.ai requires https) — e.g. Caddy auto-TLS, `/etc/caddy/Caddyfile`:
|
|
271
|
+
```
|
|
272
|
+
mcp.yourdomain.com { reverse_proxy localhost:8787 }
|
|
273
|
+
```
|
|
274
|
+
3. **Add to claude.ai** → Remote MCP server URL = `https://mcp.yourdomain.com/mcp`.
|
|
275
|
+
|
|
276
|
+
**Auth on a shared server:**
|
|
277
|
+
- **Single-account** (works with the dialog today): `WEBCAKE_JWT` in the service env → everyone using the
|
|
278
|
+
connector shares that one Webcake account. Keep the URL private / gated; the token expires (~90 days).
|
|
279
|
+
- **Per-user** (each person their own account): give each person a URL with their own `?jwt=<ljwt>` (works
|
|
280
|
+
through the dialog, but the token appears in logs), or use a header-capable client (`mcp-remote --header …`),
|
|
281
|
+
or add **OAuth** (not implemented) for the cleanest flow.
|
|
282
|
+
|
|
187
283
|
## Manual Setup (local)
|
|
188
284
|
|
|
189
285
|
```bash
|
|
@@ -197,6 +293,55 @@ npm run smoke # offline self-test of factory + validator (prints "ALL GOOD"
|
|
|
197
293
|
The reference/validation tools work with **zero config**. Env vars are only needed for the persistence
|
|
198
294
|
tools (`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizations`).
|
|
199
295
|
|
|
296
|
+
## Connect once — grab your token automatically (`login`)
|
|
297
|
+
|
|
298
|
+
Instead of copying a JWT by hand, run:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Production — zero config (defaults: connect via webcake.io, API via api.webcake.io):
|
|
302
|
+
npx -y webcake-landing-mcp login
|
|
303
|
+
|
|
304
|
+
# Local dev — point at your local SPA (5173) + API (5800):
|
|
305
|
+
node dist/index.js login \
|
|
306
|
+
--connect-url http://localhost:5173/mcp-connect \
|
|
307
|
+
--api-base http://localhost:5800
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
It opens your browser → (log into Webcake if needed) → the token is saved to
|
|
311
|
+
`~/.webcake-landing-mcp/auth.json`, which the server then reads automatically.
|
|
312
|
+
|
|
313
|
+
You're already logged in to Webcake in your browser, so `login` just opens a Webcake "connect"
|
|
314
|
+
page that reads your **`ljwt`** (landing) cookie and hands the token back to a localhost callback —
|
|
315
|
+
no copy-paste. The saved token is used by **both** the stdio server and a single-user `serve`
|
|
316
|
+
deployment (env vars still take precedence). The landing JWT lasts ~90 days, so you rarely reconnect.
|
|
317
|
+
|
|
318
|
+
Two URLs, don't mix them up:
|
|
319
|
+
|
|
320
|
+
- **Connect page = the SPA** (`--connect-url` / `WEBCAKE_CONNECT_URL`): `https://webcake.io/mcp-connect`
|
|
321
|
+
in prod, `http://localhost:5173/mcp-connect` locally. Otherwise derived from `WEBCAKE_APP_BASE` +
|
|
322
|
+
`/mcp-connect`, defaulting to `https://webcake.io/mcp-connect`.
|
|
323
|
+
- **API base = the backend** (`--api-base` / `WEBCAKE_API_BASE`): `https://api.webcake.io` in prod,
|
|
324
|
+
`http://localhost:5800` locally. Defaults to `https://api.webcake.io`.
|
|
325
|
+
|
|
326
|
+
Other flags: `--org-id`, `--port`, `--no-open`. Saved-file dir: `WEBCAKE_CONFIG_DIR` (default
|
|
327
|
+
`~/.webcake-landing-mcp`).
|
|
328
|
+
|
|
329
|
+
**Backend endpoint to add** (in your Webcake backend — it owns the session cookie):
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
GET /mcp-connect?redirect_uri=<loopback>&state=<s>
|
|
333
|
+
→ read the `ljwt` cookie (the logged-in user's landing token)
|
|
334
|
+
→ 302 to <redirect_uri>?token=<ljwt>&state=<s>
|
|
335
|
+
(if there's no cookie: 302 to the login page first, then back here)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
For safety, only honor `redirect_uri` values on `http://127.0.0.1:*` / `http://localhost:*`.
|
|
339
|
+
(Reference implementation: `builderx_spa/src/views/McpConnect.vue` reads `cookies.get('ljwt')` — so this
|
|
340
|
+
flow can also be done entirely in the SPA, no backend route needed.)
|
|
341
|
+
|
|
342
|
+
> Multi-user remote (the claude.ai connector dialog) can't do this browser loopback — there each
|
|
343
|
+
> user sends their own token via the `x-webcake-jwt` header (see the remote-connector section above).
|
|
344
|
+
|
|
200
345
|
## Environment Variables
|
|
201
346
|
|
|
202
347
|
| Variable | Required | Description |
|
|
@@ -206,6 +351,8 @@ tools (`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizatio
|
|
|
206
351
|
| `WEBCAKE_ORG_ID` | No | Default organization id for `create_page` (overridden by its `organization_id` arg). Omit → personal page. |
|
|
207
352
|
| `WEBCAKE_HOST` | No | Optional `Host` header (Phoenix routes by host, e.g. `builder.localhost`). |
|
|
208
353
|
| `WEBCAKE_APP_BASE` | No | Optional base used to build editor/preview URLs in the result. |
|
|
354
|
+
| `WEBCAKE_CONNECT_URL` | No | The SPA "connect" page for `login` (default `https://webcake.io/mcp-connect`; else `WEBCAKE_APP_BASE` + `/mcp-connect`). |
|
|
355
|
+
| `WEBCAKE_CONFIG_DIR` | No | Dir for the saved `auth.json` written by `login` (default `~/.webcake-landing-mcp`). |
|
|
209
356
|
|
|
210
357
|
> \* `WEBCAKE_API_BASE` and `WEBCAKE_JWT` are only needed for the persistence tools. The reference and
|
|
211
358
|
> validation tools (`get_generation_guide`, `list_elements`, `get_element`, `validate_page`, …) work without them.
|
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
|
|
|
@@ -184,6 +187,97 @@ Cấu hình MCP giống bản local, chỉ khác `command`/`args` trỏ tới `n
|
|
|
184
187
|
> 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
188
|
> (`webcake-landing-mcp@1.0.0`) nếu cần build tái lập được.
|
|
186
189
|
|
|
190
|
+
## Chạy như remote connector (Streamable HTTP)
|
|
191
|
+
|
|
192
|
+
Server còn nói được transport **remote MCP** (Streamable HTTP), nên có thể thêm qua dialog
|
|
193
|
+
**"Add custom connector"** của Claude bằng một URL — không chỉ stdio local.
|
|
194
|
+
|
|
195
|
+
Chạy chế độ HTTP (port mặc định `8787`, hoặc đặt `PORT` / `--port`):
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npx -y webcake-landing-mcp serve --port 8787
|
|
199
|
+
# → endpoint MCP tại http://localhost:8787/mcp (GET / hoặc /health trả JSON trạng thái)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Đưa ra **HTTPS** ở URL public (reverse proxy, tunnel như `ngrok http 8787`, hoặc host bất kỳ), rồi vào
|
|
203
|
+
Claude → **Add custom connector**:
|
|
204
|
+
|
|
205
|
+
- **Name**: `webcake-landing`
|
|
206
|
+
- **Remote MCP server URL**: `https://<host-của-bạn>/mcp`
|
|
207
|
+
|
|
208
|
+
Dialog không có ô header, nên muốn truyền token qua đó thì **để vào URL**:
|
|
209
|
+
`https://<host-của-bạn>/mcp?jwt=<ljwt>` (nhận thêm `&api_base=…`, `&org_id=…`, `&host=…`, `&app_base=…`).
|
|
210
|
+
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
|
|
211
|
+
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 ở
|
|
212
|
+
reverse proxy; dùng header (hoặc OAuth) an toàn hơn nếu client hỗ trợ.
|
|
213
|
+
|
|
214
|
+
### Auth — mỗi request, đa người dùng (không token chung)
|
|
215
|
+
|
|
216
|
+
Ở stdio JWT lấy từ env. Ở chế độ HTTP, mỗi request mang credential **riêng** của người gọi qua header,
|
|
217
|
+
nên server hosted là đa người dùng và không nhúng secret chung:
|
|
218
|
+
|
|
219
|
+
| Header | Tương ứng | Ghi chú |
|
|
220
|
+
|--------|-----------|---------|
|
|
221
|
+
| `x-webcake-jwt` (hoặc `Authorization: Bearer <jwt>`) | `WEBCAKE_JWT` | token tài khoản — gửi mỗi request |
|
|
222
|
+
| `x-webcake-org-id` | `WEBCAKE_ORG_ID` | org mặc định |
|
|
223
|
+
| `x-webcake-api-base` | `WEBCAKE_API_BASE` | thường set 1 lần qua env trên host |
|
|
224
|
+
| `x-webcake-app-base` | `WEBCAKE_APP_BASE` | base URL editor/preview |
|
|
225
|
+
|
|
226
|
+
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
|
|
227
|
+
`WEBCAKE_API_BASE` + `WEBCAKE_JWT` trong env của host và giữ URL riêng tư.
|
|
228
|
+
|
|
229
|
+
> ⚠️ Tool tham chiếu + generation (`get_generation_guide`, `list_elements`, `validate_page`, …) không cần
|
|
230
|
+
> secret; chỉ tool lưu trữ (`create_page`, `update_page`, …) dùng JWT. Request không có JWT thì các tool đó
|
|
231
|
+
> trả `missing_env` chứ không gọi mạng.
|
|
232
|
+
>
|
|
233
|
+
> 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:
|
|
234
|
+
> để 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ợ
|
|
235
|
+
> 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 đó.
|
|
236
|
+
|
|
237
|
+
### Test ở local (không cần URL public)
|
|
238
|
+
|
|
239
|
+
`localhost` không dùng được trong dialog claude.ai (Anthropic gọi URL từ server của họ). Để thử server `serve`
|
|
240
|
+
chạy trên máy:
|
|
241
|
+
|
|
242
|
+
- **MCP Inspector** (GUI — dễ nhất): `npx @modelcontextprotocol/inspector` → Transport **Streamable HTTP** →
|
|
243
|
+
URL `http://localhost:8787/mcp` → mục Headers thêm `x-webcake-jwt` (+ `x-webcake-api-base`) → Connect → bấm gọi tool.
|
|
244
|
+
- **`mcp-remote`** (dùng server remote từ client stdio như Claude Desktop, kèm header):
|
|
245
|
+
```json
|
|
246
|
+
{ "mcpServers": { "webcake-remote": { "command": "npx",
|
|
247
|
+
"args": ["-y", "mcp-remote", "http://localhost:8787/mcp",
|
|
248
|
+
"--header", "x-webcake-jwt:<ljwt>",
|
|
249
|
+
"--header", "x-webcake-api-base:https://api.webcake.io"] } } }
|
|
250
|
+
```
|
|
251
|
+
- **curl**: `initialize` (đọc header `mcp-session-id` trả về) → `tools/list` → `tools/call`, tất cả kèm
|
|
252
|
+
`Accept: application/json, text/event-stream`.
|
|
253
|
+
|
|
254
|
+
### Deploy lên VPS
|
|
255
|
+
|
|
256
|
+
1. **Build + chạy như service** — `/etc/systemd/system/webcake-mcp.service`:
|
|
257
|
+
```ini
|
|
258
|
+
[Service]
|
|
259
|
+
WorkingDirectory=/opt/webcake-landing-mcp
|
|
260
|
+
ExecStart=/usr/bin/node dist/index.js serve --port 8787
|
|
261
|
+
Environment=WEBCAKE_API_BASE=https://api.webcake.io
|
|
262
|
+
Environment=WEBCAKE_JWT=<ljwt> # chỉ cho single-account — xem ghi chú auth dưới
|
|
263
|
+
Restart=always
|
|
264
|
+
[Install]
|
|
265
|
+
WantedBy=multi-user.target
|
|
266
|
+
```
|
|
267
|
+
`sudo systemctl enable --now webcake-mcp` (build 1 lần: `npm install && npm run build`).
|
|
268
|
+
2. **HTTPS + domain** (claude.ai bắt buộc https) — vd Caddy tự cấp TLS, `/etc/caddy/Caddyfile`:
|
|
269
|
+
```
|
|
270
|
+
mcp.yourdomain.com { reverse_proxy localhost:8787 }
|
|
271
|
+
```
|
|
272
|
+
3. **Thêm vào claude.ai** → Remote MCP server URL = `https://mcp.yourdomain.com/mcp`.
|
|
273
|
+
|
|
274
|
+
**Auth trên server chia sẻ:**
|
|
275
|
+
- **Single-account** (dùng được với dialog ngay): đặt `WEBCAKE_JWT` ở env service → mọi người dùng connector
|
|
276
|
+
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).
|
|
277
|
+
- **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,
|
|
278
|
+
nhưng token lộ trong log), hoặc dùng client hỗ trợ header (`mcp-remote --header …`), hoặc thêm **OAuth**
|
|
279
|
+
(chưa làm) cho gọn nhất.
|
|
280
|
+
|
|
187
281
|
## Cài thủ công (local)
|
|
188
282
|
|
|
189
283
|
```bash
|
|
@@ -197,6 +291,55 @@ npm run smoke # self-test offline của factory + validator (in "ALL GOOD")
|
|
|
197
291
|
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
292
|
(`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizations`).
|
|
199
293
|
|
|
294
|
+
## Kết nối một lần — tự lấy token (`login`)
|
|
295
|
+
|
|
296
|
+
Thay vì copy JWT bằng tay, chạy:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Production — zero config (mặc định: connect qua webcake.io, API qua api.webcake.io):
|
|
300
|
+
npx -y webcake-landing-mcp login
|
|
301
|
+
|
|
302
|
+
# Local dev — trỏ vào SPA (5173) + API (5800) ở máy:
|
|
303
|
+
node dist/index.js login \
|
|
304
|
+
--connect-url http://localhost:5173/mcp-connect \
|
|
305
|
+
--api-base http://localhost:5800
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Nó mở browser → (đăng nhập Webcake nếu cần) → token được lưu vào
|
|
309
|
+
`~/.webcake-landing-mcp/auth.json`, server tự đọc.
|
|
310
|
+
|
|
311
|
+
Bạn đang đăng nhập Webcake sẵn trong browser, nên `login` chỉ mở trang "connect" của Webcake — trang này
|
|
312
|
+
đọc cookie **`ljwt`** (landing) và trả token về một callback loopback nội bộ — khỏi copy-paste. Token đã lưu
|
|
313
|
+
đượ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
|
|
314
|
+
ngày nên hiếm khi phải kết nối lại.
|
|
315
|
+
|
|
316
|
+
Hai URL, đừng nhầm:
|
|
317
|
+
|
|
318
|
+
- **Trang connect = SPA** (`--connect-url` / `WEBCAKE_CONNECT_URL`): `https://webcake.io/mcp-connect` ở prod,
|
|
319
|
+
`http://localhost:5173/mcp-connect` ở local. Nếu không, suy ra từ `WEBCAKE_APP_BASE` + `/mcp-connect`,
|
|
320
|
+
mặc định `https://webcake.io/mcp-connect`.
|
|
321
|
+
- **API base = backend** (`--api-base` / `WEBCAKE_API_BASE`): `https://api.webcake.io` ở prod,
|
|
322
|
+
`http://localhost:5800` ở local. Mặc định `https://api.webcake.io`.
|
|
323
|
+
|
|
324
|
+
Cờ khác: `--org-id`, `--port`, `--no-open`. Thư mục file lưu: `WEBCAKE_CONFIG_DIR` (mặc định
|
|
325
|
+
`~/.webcake-landing-mcp`).
|
|
326
|
+
|
|
327
|
+
**Endpoint cần thêm ở backend** (trong Webcake backend — nơi giữ cookie session):
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
GET /mcp-connect?redirect_uri=<loopback>&state=<s>
|
|
331
|
+
→ đọc cookie `ljwt` (landing token của user đang đăng nhập)
|
|
332
|
+
→ 302 tới <redirect_uri>?token=<ljwt>&state=<s>
|
|
333
|
+
(nếu chưa có cookie: 302 sang trang login trước, xong quay lại đây)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Để an toàn, chỉ chấp nhận `redirect_uri` ở `http://127.0.0.1:*` / `http://localhost:*`.
|
|
337
|
+
(Mẫu tham khảo: `builderx_spa/src/views/McpConnect.vue` đọc `cookies.get('ljwt')` — nên flow này làm hẳn ở
|
|
338
|
+
SPA cũng được, khỏi cần route backend.)
|
|
339
|
+
|
|
340
|
+
> 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
|
|
341
|
+
> qua header `x-webcake-jwt` (xem mục remote-connector ở trên).
|
|
342
|
+
|
|
200
343
|
## Biến môi trường
|
|
201
344
|
|
|
202
345
|
| Biến | Bắt buộc | Mô tả |
|
|
@@ -206,6 +349,8 @@ Các tool tham chiếu/kiểm tra chạy với **zero config**. Biến môi trư
|
|
|
206
349
|
| `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
350
|
| `WEBCAKE_HOST` | Không | Header `Host` tuỳ chọn (Phoenix route theo host, ví dụ `builder.localhost`). |
|
|
208
351
|
| `WEBCAKE_APP_BASE` | Không | Base tuỳ chọn để dựng URL editor/preview trong kết quả. |
|
|
352
|
+
| `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`). |
|
|
353
|
+
| `WEBCAKE_CONFIG_DIR` | Không | Thư mục chứa `auth.json` do `login` ghi (mặc định `~/.webcake-landing-mcp`). |
|
|
209
354
|
|
|
210
355
|
> \* `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
356
|
> (`get_generation_guide`, `list_elements`, `get_element`, `validate_page`, …) chạy không cần chúng.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `webcake-landing-mcp login` — grab the user's Webcake JWT automatically via the
|
|
3
|
+
* browser, no copy-paste.
|
|
4
|
+
*
|
|
5
|
+
* Flow (works for local stdio AND a single-user remote deploy):
|
|
6
|
+
* 1. open a loopback server on 127.0.0.1:<port>,
|
|
7
|
+
* 2. open the browser to the Webcake "connect" URL with redirect_uri=<loopback>,
|
|
8
|
+
* 3. the user is already logged in to Webcake, so Webcake reads their `jwt`
|
|
9
|
+
* cookie server-side and 302s back to the loopback with ?token=<jwt>,
|
|
10
|
+
* 4. we save it to the credentials file (persistence/config.ts#saveSavedConfig),
|
|
11
|
+
* which the stdio/http server then reads automatically.
|
|
12
|
+
*
|
|
13
|
+
* Backend contract (added to landing_page_backend — owned by the user):
|
|
14
|
+
* GET {WEBCAKE_CONNECT_URL}?redirect_uri=<loopback>&state=<s>
|
|
15
|
+
* → read cookie `jwt` → 302 to <redirect_uri>?token=<jwt>&state=<s>
|
|
16
|
+
* (or 302 to the login page first, then back). Restrict redirect_uri to
|
|
17
|
+
* http://127.0.0.1:* / http://localhost:* for safety.
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
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";
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const get = (name) => {
|
|
31
|
+
const i = argv.indexOf(name);
|
|
32
|
+
return i !== -1 ? argv[i + 1] : undefined;
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
connectUrl: get("--connect-url"),
|
|
36
|
+
apiBase: get("--api-base"),
|
|
37
|
+
orgId: get("--org-id"),
|
|
38
|
+
port: get("--port") ? Number(get("--port")) : undefined,
|
|
39
|
+
open: !argv.includes("--no-open"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function openBrowser(url) {
|
|
43
|
+
const platform = process.platform;
|
|
44
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
45
|
+
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
46
|
+
try {
|
|
47
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* ignore — the URL is also printed */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>Connected</title>
|
|
54
|
+
<body style="font-family:system-ui;text-align:center;padding:48px">
|
|
55
|
+
<h2>✓ Connected to Webcake</h2><p>You can close this tab and return to your terminal.</p></body>`;
|
|
56
|
+
function resolveConnectUrl(opts) {
|
|
57
|
+
if (opts.connectUrl)
|
|
58
|
+
return opts.connectUrl;
|
|
59
|
+
if (process.env.WEBCAKE_CONNECT_URL)
|
|
60
|
+
return process.env.WEBCAKE_CONNECT_URL;
|
|
61
|
+
// The connect page is on the SPA (WEBCAKE_APP_BASE), NOT the API base.
|
|
62
|
+
const appBase = process.env.WEBCAKE_APP_BASE;
|
|
63
|
+
if (appBase)
|
|
64
|
+
return `${appBase.replace(/\/+$/, "")}/mcp-connect`;
|
|
65
|
+
return DEFAULT_CONNECT_URL;
|
|
66
|
+
}
|
|
67
|
+
export async function runLogin(argv) {
|
|
68
|
+
const opts = parseArgs(argv);
|
|
69
|
+
const connectUrl = resolveConnectUrl(opts);
|
|
70
|
+
const apiBase = opts.apiBase || process.env.WEBCAKE_API_BASE || DEFAULT_API_BASE;
|
|
71
|
+
const state = randomBytes(16).toString("hex");
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
const server = createServer((req, res) => {
|
|
74
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
75
|
+
if (url.pathname !== "/callback") {
|
|
76
|
+
res.writeHead(404).end("Not found");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const token = url.searchParams.get("token");
|
|
80
|
+
if (!token || url.searchParams.get("state") !== state) {
|
|
81
|
+
res.writeHead(400, { "content-type": "text/html" }).end("<p>Invalid or expired login — re-run the command.</p>");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const path = saveSavedConfig({
|
|
85
|
+
jwt: token,
|
|
86
|
+
...(apiBase ? { base: apiBase.replace(/\/+$/, "") } : {}),
|
|
87
|
+
...(opts.orgId ? { orgId: opts.orgId } : {}),
|
|
88
|
+
savedAt: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
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
|
+
}
|
|
95
|
+
server.close();
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
server.on("error", reject);
|
|
99
|
+
server.listen(opts.port ?? 0, "127.0.0.1", () => {
|
|
100
|
+
const addr = server.address();
|
|
101
|
+
const port = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
102
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
103
|
+
const sep = connectUrl.includes("?") ? "&" : "?";
|
|
104
|
+
const full = `${connectUrl}${sep}redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}`;
|
|
105
|
+
console.error("Opening your browser to connect to Webcake (log in there if prompted):");
|
|
106
|
+
console.error(" " + full + "\n");
|
|
107
|
+
if (opts.open)
|
|
108
|
+
openBrowser(full);
|
|
109
|
+
});
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
server.close();
|
|
112
|
+
reject(new Error("login timed out after 180s."));
|
|
113
|
+
}, 180_000).unref();
|
|
114
|
+
});
|
|
115
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote transport: a Streamable-HTTP server so the MCP can be added as a Claude
|
|
3
|
+
* "custom connector" via a public URL — alongside the stdio mode in index.ts.
|
|
4
|
+
*
|
|
5
|
+
* Stateful sessions: an `initialize` POST (no session id) spins up a fresh
|
|
6
|
+
* McpServer + transport and returns an `mcp-session-id`; later requests reuse it
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* All logging stays on stderr (console.error), same as stdio mode.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { createServer as createHttpServer } from "node:http";
|
|
16
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
17
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
import { createServer } from "./server.js";
|
|
19
|
+
const MCP_PATH = "/mcp";
|
|
20
|
+
function sendJson(res, status, body) {
|
|
21
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
22
|
+
res.end(JSON.stringify(body));
|
|
23
|
+
}
|
|
24
|
+
function rpcError(res, status, message) {
|
|
25
|
+
sendJson(res, status, { jsonrpc: "2.0", error: { code: -32000, message }, id: null });
|
|
26
|
+
}
|
|
27
|
+
async function readBody(req) {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const c of req)
|
|
30
|
+
chunks.push(c);
|
|
31
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
32
|
+
return raw ? JSON.parse(raw) : undefined;
|
|
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
|
+
api_base: "x-webcake-api-base",
|
|
43
|
+
org_id: "x-webcake-org-id",
|
|
44
|
+
host: "x-webcake-host",
|
|
45
|
+
app_base: "x-webcake-app-base",
|
|
46
|
+
};
|
|
47
|
+
function applyQueryAuth(req) {
|
|
48
|
+
const q = (req.url ?? "").indexOf("?");
|
|
49
|
+
if (q === -1)
|
|
50
|
+
return;
|
|
51
|
+
const params = new URLSearchParams((req.url ?? "").slice(q + 1));
|
|
52
|
+
for (const [param, header] of Object.entries(QUERY_AUTH)) {
|
|
53
|
+
const value = params.get(param);
|
|
54
|
+
// Only fill in when there's no explicit header (header wins). The transport
|
|
55
|
+
// builds its Request from `req.rawHeaders` (via @hono/node-server), so we MUST
|
|
56
|
+
// push there — mutating `req.headers` alone is not seen by the tool handlers.
|
|
57
|
+
if (value && req.headers[header] == null) {
|
|
58
|
+
req.headers[header] = value;
|
|
59
|
+
req.rawHeaders.push(header, value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function startHttpServer(port) {
|
|
64
|
+
// mcp-session-id -> live transport (each bound to its own McpServer instance).
|
|
65
|
+
const transports = new Map();
|
|
66
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
67
|
+
const path = (req.url ?? "").split("?")[0];
|
|
68
|
+
// Lightweight health check for hosting platforms.
|
|
69
|
+
if (req.method === "GET" && (path === "/" || path === "/health")) {
|
|
70
|
+
return sendJson(res, 200, { ok: true, server: "webcake-landing", transport: "streamable-http", endpoint: MCP_PATH });
|
|
71
|
+
}
|
|
72
|
+
if (path !== MCP_PATH)
|
|
73
|
+
return rpcError(res, 404, `Not found. Send MCP requests to ${MCP_PATH}.`);
|
|
74
|
+
// Accept credentials via ?jwt=/?api_base=/... (for clients that can't set headers).
|
|
75
|
+
applyQueryAuth(req);
|
|
76
|
+
const sidHeader = req.headers["mcp-session-id"];
|
|
77
|
+
const sessionId = Array.isArray(sidHeader) ? sidHeader[0] : sidHeader;
|
|
78
|
+
try {
|
|
79
|
+
// Existing session: delegate any method (POST/GET/DELETE) to its transport.
|
|
80
|
+
if (sessionId && transports.has(sessionId)) {
|
|
81
|
+
const transport = transports.get(sessionId);
|
|
82
|
+
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
83
|
+
await transport.handleRequest(req, res, body);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// New session: only a POST `initialize` may open one.
|
|
87
|
+
if (req.method === "POST") {
|
|
88
|
+
const body = await readBody(req);
|
|
89
|
+
if (!sessionId && isInitializeRequest(body)) {
|
|
90
|
+
const transport = new StreamableHTTPServerTransport({
|
|
91
|
+
sessionIdGenerator: () => randomUUID(),
|
|
92
|
+
onsessioninitialized: (id) => {
|
|
93
|
+
transports.set(id, transport);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
transport.onclose = () => {
|
|
97
|
+
if (transport.sessionId)
|
|
98
|
+
transports.delete(transport.sessionId);
|
|
99
|
+
};
|
|
100
|
+
const server = createServer();
|
|
101
|
+
await server.connect(transport);
|
|
102
|
+
await transport.handleRequest(req, res, body);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
return rpcError(res, 400, "Bad Request: no valid mcp-session-id (send an initialize request first).");
|
|
106
|
+
}
|
|
107
|
+
return rpcError(res, 400, "Bad Request: missing or unknown mcp-session-id.");
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
console.error("[webcake-http] request error:", err);
|
|
111
|
+
if (!res.headersSent)
|
|
112
|
+
rpcError(res, 500, "Internal server error.");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
await new Promise((resolve) => httpServer.listen(port, resolve));
|
|
116
|
+
// stderr only.
|
|
117
|
+
console.error(`[webcake-elements] MCP Streamable-HTTP server ready on http://localhost:${port}${MCP_PATH}`);
|
|
118
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Webcake landing MCP server (stdio) — entry point.
|
|
4
4
|
*
|
|
5
|
-
* Thin dispatcher:
|
|
6
|
-
* bundled IDE installer
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Thin dispatcher:
|
|
6
|
+
* - `webcake-landing-mcp install|uninstall|--help` → bundled IDE installer
|
|
7
|
+
* - `webcake-landing-mcp login` → grab the Webcake JWT via the browser and save it
|
|
8
|
+
* (~/.webcake-landing-mcp/auth.json); see ./auth/login.ts
|
|
9
|
+
* - `webcake-landing-mcp serve [--port N]` (or PORT env) → remote Streamable-HTTP
|
|
10
|
+
* server (for Claude "custom connector" via a public URL); see ./http.ts
|
|
11
|
+
* - no subcommand → stdio MCP server (the default; for desktop/CLI configs)
|
|
12
|
+
* The server itself (McpServer + tool registration) is built in ./server.ts; the
|
|
13
|
+
* knowledge, factory, validator, and HTTP client live under ./core, ./domains,
|
|
14
|
+
* ./tools, and ./persistence.
|
|
10
15
|
*
|
|
11
16
|
* stdout is the MCP channel — all logging goes to stderr (console.error) only.
|
|
12
17
|
*/
|
|
@@ -27,6 +32,19 @@ async function main() {
|
|
|
27
32
|
await runInstaller(rest);
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
35
|
+
if (sub === "login") {
|
|
36
|
+
const { runLogin } = await import("./auth/login.js");
|
|
37
|
+
await runLogin(process.argv.slice(3));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (sub === "serve" || sub === "http" || sub === "serve-http") {
|
|
41
|
+
const { startHttpServer } = await import("./http.js");
|
|
42
|
+
const flagIdx = process.argv.indexOf("--port");
|
|
43
|
+
const raw = (flagIdx !== -1 ? process.argv[flagIdx + 1] : undefined) ?? process.env.PORT;
|
|
44
|
+
const port = Number(raw);
|
|
45
|
+
await startHttpServer(Number.isFinite(port) && port > 0 ? port : 8787);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
30
48
|
const transport = new StdioServerTransport();
|
|
31
49
|
const server = createServer();
|
|
32
50
|
await server.connect(transport);
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the persistence config. Three sources, in priority order:
|
|
3
|
+
* 1. per-request `overrides` (remote/Streamable-HTTP mode: each client sends its
|
|
4
|
+
* OWN Webcake JWT via HTTP headers — see `configFromHeaders` — so a hosted
|
|
5
|
+
* server is multi-user and never bakes a shared secret into env), then
|
|
6
|
+
* 2. environment variables (stdio / single-user mode), then
|
|
7
|
+
* 3. the saved credentials file written by `webcake-landing-mcp login`
|
|
8
|
+
* (~/.webcake-landing-mcp/auth.json) — so a user can connect once via the
|
|
9
|
+
* browser instead of pasting a token.
|
|
10
|
+
*
|
|
11
|
+
* The JWT is never hard-coded (the repo is public). `readConfig` returns
|
|
12
|
+
* { config: null, missing } when required values are absent so the persistence
|
|
13
|
+
* tools can report exactly what to provide.
|
|
14
|
+
*
|
|
15
|
+
* WEBCAKE_API_BASE e.g. http://localhost:5800 (required to call the backend)
|
|
16
|
+
* WEBCAKE_JWT the account JWT (required to call the backend)
|
|
17
|
+
* WEBCAKE_ORG_ID optional default organization id for create_page
|
|
18
|
+
* WEBCAKE_HOST optional Host header override (Phoenix routes by host)
|
|
19
|
+
* WEBCAKE_APP_BASE optional base for editor/preview URLs in the result
|
|
20
|
+
* WEBCAKE_CONFIG_DIR optional dir for the saved auth.json (default ~/.webcake-landing-mcp)
|
|
21
|
+
*/
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
25
|
+
export function readConfig(overrides = {}) {
|
|
26
|
+
const saved = readSavedConfig();
|
|
27
|
+
const base = overrides.base ?? process.env.WEBCAKE_API_BASE ?? saved.base;
|
|
28
|
+
const jwt = overrides.jwt ?? process.env.WEBCAKE_JWT ?? saved.jwt;
|
|
4
29
|
const missing = [];
|
|
5
30
|
if (!base)
|
|
6
31
|
missing.push("WEBCAKE_API_BASE");
|
|
@@ -12,10 +37,60 @@ export function readConfig() {
|
|
|
12
37
|
config: {
|
|
13
38
|
base: base.replace(/\/+$/, ""),
|
|
14
39
|
jwt: jwt,
|
|
15
|
-
orgId: process.env.WEBCAKE_ORG_ID,
|
|
16
|
-
host: process.env.WEBCAKE_HOST,
|
|
17
|
-
appBase: process.env.WEBCAKE_APP_BASE?.replace(/\/+$/, ""),
|
|
40
|
+
orgId: overrides.orgId ?? process.env.WEBCAKE_ORG_ID ?? saved.orgId,
|
|
41
|
+
host: overrides.host ?? process.env.WEBCAKE_HOST ?? saved.host,
|
|
42
|
+
appBase: (overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? saved.appBase)?.replace(/\/+$/, ""),
|
|
18
43
|
},
|
|
19
44
|
missing: [],
|
|
20
45
|
};
|
|
21
46
|
}
|
|
47
|
+
function header(headers, name) {
|
|
48
|
+
const v = headers?.[name];
|
|
49
|
+
return Array.isArray(v) ? v[0] : v ?? undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build request-scoped config overrides from HTTP headers. Lets a remote client
|
|
53
|
+
* send its own credentials per request instead of a server-wide env token:
|
|
54
|
+
* x-webcake-jwt the account JWT (or `Authorization: Bearer <jwt>`)
|
|
55
|
+
* x-webcake-org-id organization id
|
|
56
|
+
* x-webcake-api-base backend base URL (usually set once via env instead)
|
|
57
|
+
* x-webcake-host Host header override
|
|
58
|
+
* x-webcake-app-base editor/preview URL base
|
|
59
|
+
* Any header that is absent falls back to the corresponding env var in readConfig.
|
|
60
|
+
*/
|
|
61
|
+
export function configFromHeaders(headers) {
|
|
62
|
+
const auth = header(headers, "authorization");
|
|
63
|
+
const bearer = auth && /^Bearer\s+/i.test(auth) ? auth.replace(/^Bearer\s+/i, "").trim() : undefined;
|
|
64
|
+
return {
|
|
65
|
+
base: header(headers, "x-webcake-api-base"),
|
|
66
|
+
jwt: header(headers, "x-webcake-jwt") ?? bearer,
|
|
67
|
+
orgId: header(headers, "x-webcake-org-id"),
|
|
68
|
+
host: header(headers, "x-webcake-host"),
|
|
69
|
+
appBase: header(headers, "x-webcake-app-base"),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Directory for the saved auth file (override with WEBCAKE_CONFIG_DIR). */
|
|
73
|
+
export function configDir() {
|
|
74
|
+
return process.env.WEBCAKE_CONFIG_DIR || join(homedir(), ".webcake-landing-mcp");
|
|
75
|
+
}
|
|
76
|
+
export function savedConfigPath() {
|
|
77
|
+
return join(configDir(), "auth.json");
|
|
78
|
+
}
|
|
79
|
+
/** Read the saved credentials; {} when the file is absent or unreadable. */
|
|
80
|
+
export function readSavedConfig() {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(readFileSync(savedConfigPath(), "utf8"));
|
|
83
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Merge + persist credentials to the saved file (0600). Returns the path written. */
|
|
90
|
+
export function saveSavedConfig(partial) {
|
|
91
|
+
const dir = configDir();
|
|
92
|
+
mkdirSync(dir, { recursive: true });
|
|
93
|
+
const path = savedConfigPath();
|
|
94
|
+
writeFileSync(path, JSON.stringify({ ...readSavedConfig(), ...partial }, null, 2), { mode: 0o600 });
|
|
95
|
+
return path;
|
|
96
|
+
}
|
|
@@ -4,21 +4,27 @@
|
|
|
4
4
|
* default to dry_run=true and return a JWT-redacted request preview; they only
|
|
5
5
|
* hit the network when dry_run===false. Validation uses the injected Domain;
|
|
6
6
|
* the HTTP calls go through the Webcake client.
|
|
7
|
+
*
|
|
8
|
+
* Credentials resolve per request: in remote/Streamable-HTTP mode each call's
|
|
9
|
+
* headers (extra.requestInfo.headers) carry the client's own Webcake JWT, so a
|
|
10
|
+
* hosted server is multi-user; in stdio/single-user mode they come from env.
|
|
7
11
|
*/
|
|
8
12
|
import { z } from "zod";
|
|
9
13
|
import { text } from "../mcp/response.js";
|
|
10
|
-
import { readConfig } from "../persistence/config.js";
|
|
14
|
+
import { readConfig, configFromHeaders } from "../persistence/config.js";
|
|
11
15
|
import { buildRequestRedacted, buildUpdateRequestRedacted, createPage, listOrganizations, listPages, getPageSource, updatePageSource, } from "../persistence/webcake-client.js";
|
|
12
16
|
export function registerPersistenceTools(server, domain) {
|
|
17
|
+
// Resolve config from THIS request's headers (remote per-user JWT) first, then env.
|
|
18
|
+
const cfgFor = (extra) => readConfig(configFromHeaders(extra?.requestInfo?.headers));
|
|
13
19
|
// 8) List organizations -----------------------------------------------------
|
|
14
|
-
server.tool("list_organizations", "List the account's Webcake organizations (id, name, is_default). The default org (type===1, usually the personal workspace) is where pages normally go. Call this BEFORE create_page, show the options to the user and ask which org to use — defaulting to the is_default one. Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", {}, async () => {
|
|
15
|
-
const { config, missing } =
|
|
20
|
+
server.tool("list_organizations", "List the account's Webcake organizations (id, name, is_default). The default org (type===1, usually the personal workspace) is where pages normally go. Call this BEFORE create_page, show the options to the user and ask which org to use — defaulting to the is_default one. Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", {}, async (_args, extra) => {
|
|
21
|
+
const { config, missing } = cfgFor(extra);
|
|
16
22
|
if (!config) {
|
|
17
23
|
return text({
|
|
18
24
|
ok: false,
|
|
19
25
|
reason: "missing_env",
|
|
20
26
|
missing_env: missing,
|
|
21
|
-
hint: "Configure WEBCAKE_API_BASE and WEBCAKE_JWT in the MCP server env.",
|
|
27
|
+
hint: "Configure WEBCAKE_API_BASE and WEBCAKE_JWT in the MCP server env (stdio), or send the JWT via the x-webcake-jwt header (remote).",
|
|
22
28
|
});
|
|
23
29
|
}
|
|
24
30
|
return text(await listOrganizations(config));
|
|
@@ -37,7 +43,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
37
43
|
.boolean()
|
|
38
44
|
.optional()
|
|
39
45
|
.describe("Default TRUE — preview the request without sending. Set false to actually create."),
|
|
40
|
-
}, async ({ source, name, organization_id, dry_run }) => {
|
|
46
|
+
}, async ({ source, name, organization_id, dry_run }, extra) => {
|
|
41
47
|
const pageName = name ?? "AI Page";
|
|
42
48
|
const isDry = dry_run !== false; // default true (safe)
|
|
43
49
|
const orgId = organization_id != null ? `${organization_id}` : undefined;
|
|
@@ -52,7 +58,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
52
58
|
});
|
|
53
59
|
}
|
|
54
60
|
const parsed = domain.coerce(source);
|
|
55
|
-
const { config, missing } =
|
|
61
|
+
const { config, missing } = cfgFor(extra);
|
|
56
62
|
if (isDry) {
|
|
57
63
|
return text({
|
|
58
64
|
dry_run: true,
|
|
@@ -63,7 +69,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
63
69
|
request: config
|
|
64
70
|
? buildRequestRedacted(config, pageName, parsed, orgId)
|
|
65
71
|
: {
|
|
66
|
-
note: "Set WEBCAKE_API_BASE + WEBCAKE_JWT to enable real creation. Would POST to {WEBCAKE_API_BASE}/api/v1/ai/create_page_from_source.",
|
|
72
|
+
note: "Set WEBCAKE_API_BASE + WEBCAKE_JWT (env) or send the x-webcake-jwt header to enable real creation. Would POST to {WEBCAKE_API_BASE}/api/v1/ai/create_page_from_source.",
|
|
67
73
|
},
|
|
68
74
|
hint: "Re-run with dry_run=false to actually create the page.",
|
|
69
75
|
});
|
|
@@ -73,22 +79,22 @@ export function registerPersistenceTools(server, domain) {
|
|
|
73
79
|
created: false,
|
|
74
80
|
reason: "missing_env",
|
|
75
81
|
missing_env: missing,
|
|
76
|
-
hint: "Configure WEBCAKE_API_BASE and WEBCAKE_JWT
|
|
82
|
+
hint: "Configure WEBCAKE_API_BASE and WEBCAKE_JWT (env), or send the x-webcake-jwt header (remote), then retry.",
|
|
77
83
|
});
|
|
78
84
|
}
|
|
79
85
|
const outcome = await createPage(config, pageName, parsed, orgId);
|
|
80
86
|
return text({ created: outcome.ok, ...outcome, warnings: result.warnings });
|
|
81
87
|
});
|
|
82
88
|
// 10) List pages ------------------------------------------------------------
|
|
83
|
-
server.tool("list_pages", "List the pages owned by the account (id, name, organization_id, updated_at), most-recent first. Use it to let the user pick a page to edit (then get_page → modify → update_page). Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", {}, async () => {
|
|
84
|
-
const { config, missing } =
|
|
89
|
+
server.tool("list_pages", "List the pages owned by the account (id, name, organization_id, updated_at), most-recent first. Use it to let the user pick a page to edit (then get_page → modify → update_page). Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", {}, async (_args, extra) => {
|
|
90
|
+
const { config, missing } = cfgFor(extra);
|
|
85
91
|
if (!config)
|
|
86
92
|
return text({ ok: false, reason: "missing_env", missing_env: missing });
|
|
87
93
|
return text(await listPages(config));
|
|
88
94
|
});
|
|
89
95
|
// 11) Get page (read source) ------------------------------------------------
|
|
90
|
-
server.tool("get_page", "Fetch an existing page's decoded source tree { page, popup, settings, options, cartConfigs } so you can EDIT it. Returns name + organization_id too. Edit the returned `source`, then validate_page and update_page. Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", { page_id: z.string().describe("The page id (from list_pages or a URL).") }, async ({ page_id }) => {
|
|
91
|
-
const { config, missing } =
|
|
96
|
+
server.tool("get_page", "Fetch an existing page's decoded source tree { page, popup, settings, options, cartConfigs } so you can EDIT it. Returns name + organization_id too. Edit the returned `source`, then validate_page and update_page. Needs WEBCAKE_API_BASE + WEBCAKE_JWT.", { page_id: z.string().describe("The page id (from list_pages or a URL).") }, async ({ page_id }, extra) => {
|
|
97
|
+
const { config, missing } = cfgFor(extra);
|
|
92
98
|
if (!config)
|
|
93
99
|
return text({ ok: false, reason: "missing_env", missing_env: missing });
|
|
94
100
|
return text(await getPageSource(config, page_id));
|
|
@@ -100,7 +106,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
100
106
|
.any()
|
|
101
107
|
.describe("The full edited page source { page, popup, settings, options, cartConfigs } (object or JSON string)."),
|
|
102
108
|
dry_run: z.boolean().optional().describe("Default TRUE — preview without sending. Set false to actually save."),
|
|
103
|
-
}, async ({ page_id, source, dry_run }) => {
|
|
109
|
+
}, async ({ page_id, source, dry_run }, extra) => {
|
|
104
110
|
const isDry = dry_run !== false;
|
|
105
111
|
const result = domain.validate(source);
|
|
106
112
|
if (!result.valid) {
|
|
@@ -113,7 +119,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
113
119
|
});
|
|
114
120
|
}
|
|
115
121
|
const parsed = domain.coerce(source);
|
|
116
|
-
const { config, missing } =
|
|
122
|
+
const { config, missing } = cfgFor(extra);
|
|
117
123
|
if (isDry) {
|
|
118
124
|
return text({
|
|
119
125
|
dry_run: true,
|
|
@@ -123,7 +129,7 @@ export function registerPersistenceTools(server, domain) {
|
|
|
123
129
|
missing_env: missing,
|
|
124
130
|
request: config
|
|
125
131
|
? buildUpdateRequestRedacted(config, page_id, parsed)
|
|
126
|
-
: { note: "Set WEBCAKE_API_BASE + WEBCAKE_JWT to enable real updates." },
|
|
132
|
+
: { note: "Set WEBCAKE_API_BASE + WEBCAKE_JWT (env) or send the x-webcake-jwt header to enable real updates." },
|
|
127
133
|
hint: "Re-run with dry_run=false to actually save the edit.",
|
|
128
134
|
});
|
|
129
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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": {
|