brewpage-mcp 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ Claude Code: `claude mcp add --transport http brewpage --scope user https://brew
38
38
  For clients that only start local servers; the same tools as the server URL.
39
39
 
40
40
  ```bash
41
- npx -y brewpage-mcp@1.22.0
41
+ npx -y brewpage-mcp@1.23.0
42
42
  ```
43
43
 
44
44
  Requires Node.js 20 or newer. This starts the stdio server for an MCP client.
@@ -63,7 +63,7 @@ Local fallback (stdio, needs Node.js; the same tools): add to `~/Library/Applica
63
63
  "mcpServers": {
64
64
  "brewpage": {
65
65
  "command": "npx",
66
- "args": ["-y", "brewpage-mcp@1.22.0"]
66
+ "args": ["-y", "brewpage-mcp@1.23.0"]
67
67
  }
68
68
  }
69
69
  }
@@ -86,7 +86,7 @@ Custom servers follow your ChatGPT plan and workspace settings.
86
86
  The remote command above is the default. Local fallback: register a project-scoped stdio server:
87
87
 
88
88
  ```bash
89
- claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.22.0
89
+ claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.23.0
90
90
  ```
91
91
 
92
92
  This writes `.mcp.json` in the project root. Use `--scope user` for user-scoped registration. See the [official Claude Code MCP guide](https://code.claude.com/docs/en/mcp).
@@ -110,7 +110,7 @@ Local fallback (stdio, needs Node.js; the same tools): open **Settings > MCP** a
110
110
  "mcpServers": {
111
111
  "brewpage": {
112
112
  "command": "npx",
113
- "args": ["-y", "brewpage-mcp@1.22.0"]
113
+ "args": ["-y", "brewpage-mcp@1.23.0"]
114
114
  }
115
115
  }
116
116
  }
@@ -124,7 +124,7 @@ Open the Cline MCP settings panel and add:
124
124
  {
125
125
  "brewpage": {
126
126
  "command": "npx",
127
- "args": ["-y", "brewpage-mcp@1.22.0"]
127
+ "args": ["-y", "brewpage-mcp@1.23.0"]
128
128
  }
129
129
  }
130
130
  ```
@@ -170,7 +170,7 @@ Thirty-two tools are available. Creation returns an owner token; keep it to modi
170
170
 
171
171
  All five publish tools accept optional `deliveryMode: "path" | "subdomain"`, sent only as `X-Delivery-Mode`; authored JSON and multipart bodies keep their existing format. Visibility and password protection are separate choices. Omitted namespace is private (a generated `priv-<random>` namespace): unlisted and not indexed; anyone with the link can still open it. `public` lists HTML, sites and files in the gallery and sitemap, so Google, Bing and AI search can find and index them; public pages are served on the brewpage.app domain for the strongest reach (a site always gets its own subdomain). Never put secrets in a public publication. Password-protected publications, JSON and KV are never listed.
172
172
 
173
- The MCP server wraps a subset of the backend-generated public [REST contract](../openapi/openapi.json). TTL inputs take days or a string such as `2w`/`36h` (default 15); the server rounds to whole days and clamps to 1–30 (owner-specific caps and eligible automatic extensions can differ). Use returned `expiresAt`. Every tolerated-input adjustment (TTL clamp, format alias, skipped site member, normalised namespace) comes back in the response `notices` array and is printed as `Notice (code): message`; `secret-detected` prints as `WARNING (code): message`: relay it to the user. A live BrewPage token or manage link in public content (`credential-published`) prints first as `SECURITY ALERT (credential-published): …`: tell the user to remove and reset it; that result is hidden from the gallery, so it prints no `VISIBILITY` line. A minted `manageUrl` prints as `MANAGE LINK (private): …`: give it to the user privately, never publish it. A listed public result prints `VISIBILITY: public, listed. Hide: set_visibility listed=false`. `get_page` reads stored rendered HTML or format-specific code/text from the API; it does not call the owner-only original-source endpoint or a browser viewer. `publish_file` uploads from a source URL and does not expose password or TTL inputs.
173
+ The MCP server wraps a subset of the backend-generated public [REST contract](../openapi/openapi.json). TTL inputs are days: up to 30 days without an account, up to 90 with a free profile (an owner token held in the profile); a default applies when omitted; whole days such as `15`, `15d` or `15 days`. Use returned `expiresAt`. Every tolerated-input adjustment (format alias, skipped site member, normalised namespace) comes back in the response `notices` array and is printed as `Notice (code): message`; `secret-detected` prints as `WARNING (code): message`: relay it to the user. A live BrewPage token or manage link in public content (`credential-published`) prints first as `SECURITY ALERT (credential-published): …`: tell the user to remove and reset it; that result is hidden from the gallery, so it prints no `VISIBILITY` line. A minted `manageUrl` prints as `MANAGE LINK (private): …`: give it to the user privately, never publish it. A listed public result prints `VISIBILITY: public, listed. Hide: set_visibility listed=false`. `get_page` reads stored rendered HTML or format-specific code/text from the API; it does not call the owner-only original-source endpoint or a browser viewer. `publish_file` uploads from a source URL and does not expose password or TTL inputs.
174
174
 
175
175
  For a fresh publication, sites and HTML pages of format `HTML` outside `public` (including the generated unlisted namespace) always use Dedicated subdomain (`subdomain`); omit `deliveryMode` there. An explicit `path` there is not rejected: the publication is created on its subdomain and the result reports `deliveryModeMatched: false`, `deliveryModeReason: "policy:subdomain-required"` and a `Delivery notice:` line. Everything else in any namespace (Markdown, code/text, files, JSON, KV, public HTML) defaults to Promotion (`path`) and may choose Dedicated subdomain, including password-protected content. Promotion uses a BrewPage path with stricter active-document restrictions. Each NEW site gets one publication-specific host for its complete bundle, preserving relative/root-relative paths; missing assets return 404, with no SPA fallback. Third-party widgets may still need provider configuration.
176
176
 
@@ -198,7 +198,7 @@ update_hosting(type="html", namespace="public", id="aBcDeFgHiJ", ownerToken="tok
198
198
 
199
199
  Publish HTML, Markdown, code or JSON content to BrewPage; with `format` omitted the server detects it. Returns a public URL and owner token. Markdown raw HTML is rendered through an allowlist: scripts, styles, event handlers, forms, frames/embeds and `meta`/`base`/`link` are removed; links allow `http`, `https`, `mailto`, `tel` and relative URLs, images also `data:`; a lone unknown tag such as `<author>` shows as text.
200
200
 
201
- Parameters: `content` (string), `format` (optional; omit to let the server detect HTML, Markdown, code or JSON; `html` keeps code/JSON as HTML; also `markdown`/`md` or a code language), `namespace` (optional -- **omit for private, unlisted content**; `public` lists the page in the gallery and sitemap for Google, Bing and AI search, served on the brewpage.app domain; never put secrets there), `password` (optional), `ttlDays` (days or a string such as `2w`; default 15; the server clamps to 1--30, up to 90 with an owner token in a profile), `filename` (optional, used as title fallback), `showTopBar` (optional boolean -- adds a toolbar with filename, Download button, and theme toggle), `deliveryMode` (optional), `domain` (optional; a domain you own to connect to an HTML-format page, see Custom domains), `state` (optional; lets an HTML page save what its visitors enter, see Page data).
201
+ Parameters: `content` (string), `format` (optional; omit to let the server detect HTML, Markdown, code or JSON; `html` keeps code/JSON as HTML; also `markdown`/`md` or a code language), `namespace` (optional -- **omit for private, unlisted content**; `public` lists the page in the gallery and sitemap for Google, Bing and AI search, served on the brewpage.app domain; never put secrets there), `password` (optional), `ttlDays` (days: up to 30 without an account, up to 90 with a free profile; a default applies when omitted), `filename` (optional, used as title fallback), `showTopBar` (optional boolean -- adds a toolbar with filename, Download button, and theme toggle), `deliveryMode` (optional), `domain` (optional; a domain you own to connect to an HTML-format page, see Custom domains), `state` (optional; lets an HTML page save what its visitors enter, see Page data).
202
202
 
203
203
  The result includes `deliveryModeReason` and a `Delivery notice:` line when the server returns them (see above).
204
204
 
@@ -276,7 +276,7 @@ Publish a single-page or multi-file HTML site. Pass `entryContent` for a single
276
276
 
277
277
  Limits (current values from `get_limits`): a request is at most 25 MiB; a site at most 200 files, 5 MiB per file and 20 MiB in total; each path at most 380 characters. System files such as `.DS_Store` and `__MACOSX/` are skipped and never count; files with an unsupported extension or a too-long path are skipped and reported in notices. Entry: `entry`, else root `index.html`, else the shallowest HTML file. A site limit hit answers 400 with a message naming the limit, as does a PDF/Office file that is unreadable or password-protected; 413 means the request is over 25 MiB, has too many files and form fields, or a file name/path is too long; 422 means no servable file is left; 503 means busy: retry after `Retry-After` seconds (failed publish tools print it).
278
278
 
279
- Parameters: exactly one of `entryContent` (string) or non-empty `files` (array of `{path, content}`), `entry` (optional entry file path, default `index.html`), `namespace` (optional -- omit for private, unlisted content; `public` lists the site in the gallery and sitemap for Google, Bing and AI search; a site always gets its own subdomain), `password` (optional), `ttlDays` (days or a string such as `2w`; default 15; the server clamps to 1--30, up to 90 with an owner token in a profile), `ownerToken` (optional, groups site under an existing owner), `deliveryMode` (optional; fresh sites require `subdomain`), `domain` (optional; a domain you own to connect to the site, see Custom domains), `state` (optional; lets the site save what its visitors enter, see Page data).
279
+ Parameters: exactly one of `entryContent` (string) or non-empty `files` (array of `{path, content}`), `entry` (optional entry file path, default `index.html`), `namespace` (optional -- omit for private, unlisted content; `public` lists the site in the gallery and sitemap for Google, Bing and AI search; a site always gets its own subdomain), `password` (optional), `ttlDays` (days: up to 30 without an account, up to 90 with a free profile; a default applies when omitted), `ownerToken` (optional, groups site under an existing owner), `deliveryMode` (optional; fresh sites require `subdomain`), `domain` (optional; a domain you own to connect to the site, see Custom domains), `state` (optional; lets the site save what its visitors enter, see Page data).
280
280
 
281
281
  Example prompt:
282
282
 
@@ -306,7 +306,7 @@ republish_site(namespace="public", id="aBcDeFgHiJ", ownerToken="tok_...", files=
306
306
 
307
307
  Publish a JSON document to BrewPage. Returns a public URL and owner token. The body may be passed as a JSON string or as a structured object.
308
308
 
309
- Parameters: `json` (string, object, or array), `namespace` (optional -- omit for private content; `public` is the shared public namespace; JSON is never listed or indexed, anyone with the link can open it), `password` (optional), `ttlDays` (days or a string such as `2w`; default 15; the server clamps to 1--30, up to 90 with an owner token in a profile), `tags` (optional `string[]`), `ownerToken` (optional existing owner), `deliveryMode` (optional).
309
+ Parameters: `json` (string, object, or array), `namespace` (optional -- omit for private content; `public` is the shared public namespace; JSON is never listed or indexed, anyone with the link can open it), `password` (optional), `ttlDays` (days: up to 30 without an account, up to 90 with a free profile; a default applies when omitted), `tags` (optional `string[]`), `ownerToken` (optional existing owner), `deliveryMode` (optional).
310
310
 
311
311
  Example prompt:
312
312
 
@@ -356,7 +356,7 @@ update_json(namespace="public", id="aBcDeFgHiJ", json={"mode": "dark", "version"
356
356
 
357
357
  Create a new KV (key/value) entry. Returns a public URL and owner token. The KV entry is addressed by `(namespace, id, key)`; this tool creates the entry and emits the generated `id` along with the owner token.
358
358
 
359
- Parameters: `key` (string), `value` (string), `namespace` (optional -- omit for private content; `public` is the shared public namespace; KV is never listed or indexed, anyone with the link can open it), `password` (optional), `ttlDays` (days or a string such as `2w`; default 15; the server clamps to 1--30, up to 90 with an owner token in a profile), `tags` (optional `string[]`), `ownerToken` (optional existing owner), `deliveryMode` (optional).
359
+ Parameters: `key` (string), `value` (string), `namespace` (optional -- omit for private content; `public` is the shared public namespace; KV is never listed or indexed, anyone with the link can open it), `password` (optional), `ttlDays` (days: up to 30 without an account, up to 90 with a free profile; a default applies when omitted), `tags` (optional `string[]`), `ownerToken` (optional existing owner), `deliveryMode` (optional).
360
360
 
361
361
  Example prompt:
362
362
 
@@ -595,7 +595,7 @@ The four page-data tools below take `namespace` + `id` + `ownerToken`, or `manag
595
595
 
596
596
  ### `get_guide`
597
597
 
598
- Read a short reference text. Topic `page-data`: how a page saves what visitors enter, with the script calls and the `state` settings. Meant to be read before writing such a page. Topic `custom-domain`: the steps that put a page or site on a domain you own, with the statuses and the fix for each problem. No API call.
598
+ Read a short reference text. Topic `page-data`: how a page saves what visitors enter, with the script calls, the `state` settings, the owner view and how to try the page locally before publishing. Meant to be read before writing such a page. Topic `custom-domain`: the steps that put a page or site on a domain you own, with the statuses and the fix for each problem. No API call.
599
599
 
600
600
  Parameters: `topic` (`page-data` | `custom-domain`).
601
601
 
@@ -769,6 +769,10 @@ Every publish response includes an **owner token** -- the only credential that a
769
769
 
770
770
  ## Changelog
771
771
 
772
+ ## 1.23.0 -- 2026-11-14
773
+
774
+ - Page data guide (`get_guide` topic `page-data`) regenerated from BrewPage's new Browser SDK text: build, try locally, publish. It adds the local preview (a page opened from disk or localhost keeps entries in that browser and sends nothing: `s.local`, `?bp-local=owner`, `?bp-local=off`; the same file works unchanged once published with `state`) and the owner view (`s.role`, `s.onRole`, `list({all: true})`, `removeById`, error `not_owner`). Custom domains: 400 `DOMAIN_NOT_REGISTERED` and the notice `domain-not-registered` name the next step (buy the domain from a registrar first, or check its spelling); the `NOT_ELIGIBLE` reasons `EXPIRED` and `MODE` name the server's next step; the `DOMAIN_TAKEN` and `CHECK_PAUSED` problem texts follow the site. `ttlDays` states the contract. Thirty-two tools. Contract 2.36.0. `User-Agent: brewpage-mcp/1.23.0`.
775
+
772
776
  ## 1.22.0 -- 2026-11-12
773
777
 
774
778
  - Custom domains: the texts now lead to a connected domain. A refusal names its next step (an owner token in no profile: sign in at https://account.brewpage.app/tokens, press Add existing token, paste it, repeat); results give the DNS records step (mail records stay, a same-name A, AAAA or CNAME is replaced, Cloudflare Proxy status DNS only), one `check_domain`, then status reads until `active`; the `Problem:` line names the fix; status `lost` is described. `list_domains` takes an optional `type` + `namespace` + `id` or `hostname` and reads that one custom domain without a DNS check; without them it lists the profile as before. `get_guide` adds the topic `custom-domain`. Thirty-two tools. Contract 2.28.0. `User-Agent: brewpage-mcp/1.22.0`.
@@ -15,12 +15,12 @@ export const DOMAIN_PROBLEMS = {
15
15
  "ROUTING_WRONG": "the name points elsewhere: replace its A or CNAME record (Cloudflare: DNS only)",
16
16
  "FOREIGN_AAAA": "delete the AAAA record listed in `records[]`",
17
17
  "CAA_BLOCKS": "add the CAA record listed in `records[]`",
18
- "DOMAIN_TAKEN": "this domain is already connected: use another hostname or write to info@brewpage.app",
18
+ "DOMAIN_TAKEN": "this hostname is not available: use another hostname or write to info@brewpage.app",
19
19
  "QUEUE_FULL": "DNS records are right, verification waits its turn: nothing to do",
20
20
  "CERT_PENDING": "HTTPS certificate on its way: nothing to do",
21
21
  "CERT_FAILED": "the certificate could not be issued: check the DNS records, then check again",
22
22
  "DNS_LOST": "DNS records stayed broken past `graceEndsAt`, so the domain was disconnected: restore them, then check again",
23
- "CHECK_PAUSED": "automatic checks paused after a long failure: fix DNS, then check again (resumes them)",
23
+ "CHECK_PAUSED": "automatic checks paused, records not right a week after adding: fix DNS, then check again (resumes them)",
24
24
  "RESOLVER_UNAVAILABLE": "DNS did not answer: retried automatically",
25
25
  "PUBLICATION_GONE": "the publication is gone: connect the custom domain to another one",
26
26
  };
@@ -1,4 +1,4 @@
1
1
  /** Page data cheat-sheet: the text block under "## Page data" -> "### Browser SDK" of llms-full.txt. */
2
2
  export declare const PAGE_DATA_GUIDE: string;
3
- /** Page script call shapes: the "bp.state() -> s|null" line of llms.txt. */
3
+ /** Page script call shapes: the "bp.state() -> s|null" line of llms-full.txt. */
4
4
  export declare const PAGE_DATA_CALLS = "bp.state() -> s|null (guard it); c=s.col(\"name\"); c.set(key,value) c.get(key) c.remove(key) c.add(value) c.list({order,limit}) c.stats({countBy})";
@@ -1,53 +1,48 @@
1
- // Generated by scripts/sync-guide.mjs from brewpage-app frontend/src (llms-full.txt, llms.txt); do not hand-edit.
1
+ // Generated by scripts/sync-guide.mjs from brewpage-app frontend/src (llms-full.txt); do not hand-edit.
2
2
  /** Page data cheat-sheet: the text block under "## Page data" -> "### Browser SDK" of llms-full.txt. */
3
3
  export const PAGE_DATA_GUIDE = [
4
- "BrewPage page data (the `state` API): a published page saves data on BrewPage. No keys, no backend.",
5
- "1) Publish with a \"state\" field (POST /api/html JSON body; multipart field \"state\" on /api/sites):",
6
- " {\"content\":\"<!doctype html>...\",\"state\":{\"collections\":{",
7
- " \"saves\":{\"mode\":\"private\"},",
8
- " \"votes\":{\"mode\":\"private\",\"choices\":{\"choice\":[\"a\",\"b\",\"c\"]}},",
9
- " \"guestbook\":{\"mode\":\"shared\",\"maxDocsPerVisitor\":5},",
10
- " \"scores\":{\"mode\":\"shared\",\"index\":\"score\",\"maxDocsPerVisitor\":1},",
11
- " \"menu\":{\"mode\":\"shared\",\"write\":\"author\"}}}}",
12
- " The page gets its own address https://<label>.brewpage.app. Saving needs a declared \"state\" and that address (or its custom domain).",
13
- " Raw-HTML publishes cannot carry state: use the JSON body, or PATCH /api/{ns}/{id}/state afterwards.",
14
- "2) Include before your code (absolute URL, classic script, no async, no import):",
15
- " <script src=\"https://brewpage.app/js/state.js\"></script>",
16
- "3) Use, inside an async function (top-level return/await break a classic script):",
17
- " async function start() {",
18
- " const s = await bp.state(); // null if unavailable; reason in bp.state.reason + console",
19
- " if (!s) return showNote(\"Saving is not available on this page right now.\"); // always guard; the page keeps working",
20
- " const saves = s.col(\"saves\");",
21
- " await saves.set(\"slot1\", {level: 3}); // own doc; key A-Za-z0-9_.- (not starting with _), <=128",
22
- " const game = await saves.get(\"slot1\"); // value or null",
23
- " await saves.remove(\"slot1\"); // true if it existed",
24
- " await s.col(\"guestbook\").add({text: input.value}); // random key",
25
- " const docs = await s.col(\"guestbook\").list({order: \"-createdAt\", limit: 20}); // array; docs.next = cursor or null",
26
- " for (const d of docs) { const li = document.createElement(\"li\"); li.append(bp.text(d.value.text)); ul.append(li); }",
27
- " await s.col(\"scores\").list({order: \"-index\", limit: 10});",
28
- " await s.col(\"votes\").stats({countBy: \"choice\"}); // {count, by: {a, b, c, other}}",
29
- " await s.col(\"menu\").get(\"today\"); // write:\"author\" docs: visitors read",
30
- " }",
31
- " start();",
32
- " bp.storage.ready.then(() => bp.storage.setItem(\"k\", \"v\")); // localStorage-like, synced, one doc; works where s is null",
33
- "Modes: private (own docs only; default) | shared (all read, own write) | shared + write:\"author\" (author writes via API/MCP).",
34
- "Collection names: a-z, 0-9, _ and -, start with a letter, <=32 chars, <=32 collections.",
35
- "One thing per visitor (vote, RSVP, best score): use a fixed key such as \"me\".",
36
- "Writes run in call order; save on change or submit, not on every keystroke.",
37
- "Errors reject with e.code (e.message explains): rate_limited (e.retryAfter), quota, too_large, value_refused, mode_forbidden,",
38
- " challenge_cancelled, challenge_failed, challenge_unavailable, not_found, invalid_argument, network, unavailable, error.",
39
- "BrewPage may ask a visitor for a quick human check before saving; the SDK shows it and retries.",
40
- " If your page sets a Content-Security-Policy, allow https://challenges.cloudflare.com (script-src, frame-src).",
41
- "Limits: 16 KiB per doc (maxDocBytes up to 64 KiB); per visitor 20 docs, 32 KiB, 30 writes/min; list <= 100; data lives as long as the page.",
42
- "Identity: one cookie per visitor per page; another browser or cleared cookies = new visitor.",
43
- "bp.state() is null: without \"state\", on brewpage.app/... path addresses (needs_subdomain; unavailable in the brewpage.app viewer),",
44
- " in frames on other sites (embedded), with cookies off and outside BrewPage (file://, localhost, chat previews).",
45
- " Test the published link.",
46
- "Privacy: the author reads all visitors' data (Data tab, API). Tell visitors: \"What you enter is saved on BrewPage and can be read by this page's author.\"",
4
+ "BrewPage Page data (API word `state`): a published page saves what visitors enter. No backend, no key in page code; not KV/JSON (your own data).",
5
+ "1) Build. Before your code (absolute URL, classic script, no async/import): <script src=\"https://brewpage.app/js/state.js\"></script>",
6
+ "bp.state() -> s|null (guard it); c=s.col(\"name\"); c.set(key,value) c.get(key) c.remove(key) c.add(value) c.list({order,limit}) c.stats({countBy})",
7
+ " async function start() { // no top-level await/return",
8
+ " const s = await bp.state(); // null -> reason in bp.state.reason, console",
9
+ " if (!s) return showNote(\"Saving is not available on this page right now.\"); // always guard",
10
+ " const book = s.col(\"guestbook\");",
11
+ " await book.add({text: input.value}); // random key",
12
+ " const docs = await book.list({order: \"-createdAt\", limit: 20}); // [{key, id, value, mine, createdAt}], .next|null -> list({cursor})",
13
+ " for (const d of docs) ul.append(bp.text(d.value.text));",
14
+ " const v = s.col(\"votes\");",
15
+ " await v.set(\"me\", {choice: \"a\"}); // key A-Za-z0-9_.-, no leading _, <=128; \"me\" = one per visitor",
16
+ " await v.get(\"me\"); // value or null; remove(key) -> true if it existed",
17
+ " await v.stats({countBy: \"choice\"}); // {count, by: {a, b, other}}",
18
+ " }",
19
+ " start();",
20
+ " bp.storage.ready.then(() => bp.storage.setItem(\"k\", \"v\")); // localStorage-like, synced; works where s is null",
21
+ "2) state (in the publish body, not the page): {} = all lists private (visitor sees own entries). Name a list to change its rule:",
22
+ " {\"collections\":{\"guestbook\":{\"mode\":\"shared\",\"maxDocsPerVisitor\":5}}} = all read, each writes own",
23
+ " \"scores\":{\"mode\":\"shared\",\"index\":\"score\",\"maxDocsPerVisitor\":1} -> list({order: \"-index\"})",
24
+ " \"votes\":{\"choices\":{\"choice\":[\"a\",\"b\"]}} -> stats, other values count as other",
25
+ " \"menu\":{\"mode\":\"shared\",\"write\":\"author\"} = visitors read, author writes (Owner API, MCP)",
26
+ " List names a-z0-9_- (letter first, <=32 chars), <=32 lists. A private list never turns shared: new name.",
27
+ "3) Owner view: s.role is \"owner\" where the page shows its owner gear, else \"visitor\"; s.onRole(fn(role)) runs now and on change.",
28
+ " Owner: c.list({all: true}) = all entries + d.visitor label (paged like list); c.removeById(entry.id). No secrets in page.",
29
+ "4) Test locally: open from disk or http(s)://localhost, 127.0.0.1, [::1], *.localhost: bp.state() = local preview, s.local === true,",
30
+ " console \"[BrewPage] Local preview\". Kept in this browser, nothing sent; clear: localStorage.removeItem(\"__bp_state_local\").",
31
+ " One visitor; checks and limits as published, e.status 0. Not applied: state rules, rate limits, human check, cookies.",
32
+ " ?bp-local=owner = owner view, ?bp-local=off = off. Same file works unchanged once published with state.",
33
+ "5) Publish: POST https://brewpage.app/api/html, Content-Type: application/json, body {\"content\":\"<html>...\",\"state\":{...}}; sites: multipart field \"state\".",
34
+ " Raw HTML body cannot carry state (or PATCH /api/{ns}/{id}/state later). Own address https://<label>.brewpage.app (or custom domain).",
35
+ " Give the user the page link (`link`) and the management link (`manageUrl`, private; Data tab shows entries).",
36
+ " On the published link: one save works and shows in the Data tab; each list behaves as declared.",
37
+ "6) Errors: e.code: rate_limited (e.retryAfter), quota, too_large, value_refused, mode_forbidden, not_owner, challenge_cancelled,",
38
+ " challenge_failed, challenge_unavailable, not_found, invalid_argument, network, unavailable, error.",
39
+ " Own CSP: allow https://challenges.cloudflare.com (script-src, frame-src).",
40
+ "7) Limits: 16 KiB per entry; per visitor 20 entries, 32 KiB, 30 writes/min; list <= 100.",
41
+ " Visitor = one cookie per browser per page. Save on change or submit.",
42
+ " null: no \"state\", brewpage.app/... paths (needs_subdomain), frames on other sites (embedded), cookies off, chat previews.",
43
+ "8) Tell visitors: \"What you enter is saved on BrewPage and can be read by this page's author.\"",
47
44
  " Shared lists: \"Your message is public: everyone who opens this page can read it.\"",
48
- "Never collect passwords, card numbers or other credentials. Entries are text and numbers.",
49
- "Author API (owner token or management key): GET/PATCH /api/{ns}/{id}/state, GET /api/{ns}/{id}/state/{col}?format=csv.",
50
- "After publishing, give the user the page link and the management link (private); its Data tab shows the entries.",
45
+ " Never collect passwords, card numbers or other credentials. Render entries as text (bp.text), never innerHTML.",
51
46
  ].join("\n") + "\n";
52
- /** Page script call shapes: the "bp.state() -> s|null" line of llms.txt. */
47
+ /** Page script call shapes: the "bp.state() -> s|null" line of llms-full.txt. */
53
48
  export const PAGE_DATA_CALLS = "bp.state() -> s|null (guard it); c=s.col(\"name\"); c.set(key,value) c.get(key) c.remove(key) c.add(value) c.list({order,limit}) c.stats({countBy})";
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare const DEFAULT_BASE_URL = "https://brewpage.app";
3
- export declare const USER_AGENT = "brewpage-mcp/1.22.0";
3
+ export declare const USER_AGENT = "brewpage-mcp/1.23.0";
4
4
  export declare const OWNER_TOKEN_ENV = "BREWPAGE_OWNER_TOKEN";
5
5
  /** Server instructions of the handshake: one paragraph per line; the first stands alone within 512 chars. */
6
6
  export declare const SERVER_INSTRUCTIONS: string;
package/dist/tools.js CHANGED
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import { DOMAIN_PROBLEMS, DOMAIN_STATUSES } from "./guides/custom-domain.js";
4
4
  import { PAGE_DATA_CALLS, PAGE_DATA_GUIDE } from "./guides/page-data.js";
5
5
  export const DEFAULT_BASE_URL = "https://brewpage.app";
6
- export const USER_AGENT = "brewpage-mcp/1.22.0";
6
+ export const USER_AGENT = "brewpage-mcp/1.23.0";
7
7
  export const OWNER_TOKEN_ENV = "BREWPAGE_OWNER_TOKEN";
8
8
  /** Server instructions of the handshake: one paragraph per line; the first stands alone within 512 chars. */
9
9
  export const SERVER_INSTRUCTIONS = [
@@ -223,8 +223,8 @@ const DOMAIN_REASONS = {
223
223
  TYPE: "only HTML pages and sites can",
224
224
  FORMAT: "the page is not format html: publish it as format html",
225
225
  PASSWORD: "remove the password (management link, or POST /api/{html|sites}/{ns}/{id}/password {\"password\":null})",
226
- EXPIRED: "it has expired: connect the custom domain to another publication",
227
- MODE: "it left its own subdomain: change its hosting back to subdomain (update_hosting deliveryMode=subdomain)",
226
+ EXPIRED: "it has expired: extend it, or connect the custom domain to another publication",
227
+ MODE: "it is not on its own subdomain: set the domain on it again (set_domain with the same hostname), which moves it to its own subdomain",
228
228
  };
229
229
  const DOMAIN_READ_STATUS = "read the status with list_domains (same type, namespace and id, or hostname)";
230
230
  const DOMAIN_NEXT_CREATE = "Next: give the user exactly these DNS records to create at their DNS provider (`host` is the name DNS panels ask for). " +
@@ -316,6 +316,8 @@ function domainNeedsProfile(fromEnv, then) {
316
316
  `and ask them to sign in at ${DOMAIN_TOKENS_PAGE} (free), press "Add existing token" and paste it; then ${then}. With an admin profile token bind_token adds it instead.`;
317
317
  }
318
318
  const DOMAIN_LIMIT_NEXT = "remove_domain frees one; a hostname already in the profile moves here with set_domain.";
319
+ /** 400 `DOMAIN_NOT_REGISTERED` and its create notice; the caller appends the call to make next. */
320
+ const DOMAIN_NOT_REGISTERED_NEXT = "The domain is not registered yet: ask the user to buy it from a domain registrar first (BrewPage does not sell domains), or check its spelling;";
319
321
  /** 409 `DOMAIN_PROFILE_AMBIGUOUS` and its create notice; `then` = the call to make with the profile token. */
320
322
  function domainAmbiguous(then) {
321
323
  return `The token is in several profiles: ${then} with a profile token of the profile that should hold the custom domain (minted at ${DOMAIN_TOKENS_PAGE}).`;
@@ -330,6 +332,8 @@ function publishDomainNext(code, fromEnv) {
330
332
  return ` ${domainAmbiguous("call set_domain")}`;
331
333
  if (code === "domain-not-eligible" || code === "domain-invalid")
332
334
  return " The domain notice above says what to fix; then set_domain connects it.";
335
+ if (code === "domain-not-registered")
336
+ return ` ${DOMAIN_NOT_REGISTERED_NEXT} then set_domain connects it.`;
333
337
  if (code === "domain-account-restricted")
334
338
  return " The domain notice above says why.";
335
339
  return " The domain notice above says why. set_domain connects it later.";
@@ -358,6 +362,7 @@ function domainHints(data, address, fromEnv = false) {
358
362
  case "DOMAIN_NOT_FOUND": return ["No custom domain there: list_domains shows the profile's custom domains; set_domain connects one."];
359
363
  case "INVALID_HOSTNAME": return ["Pass one bare hostname such as www.example.com or example.com (no scheme, path or port); the message says what to fix."];
360
364
  case "DOMAIN_NOT_ALLOWED": return ["brewpage.app addresses and some domains cannot be used: ask the user for a domain they own."];
365
+ case "DOMAIN_NOT_REGISTERED": return [`${DOMAIN_NOT_REGISTERED_NEXT} then repeat this call.`];
361
366
  case "NOT_ELIGIBLE": return typeof reason === "string" && DOMAIN_REASONS[reason] ? [`This publication cannot take a custom domain: ${DOMAIN_REASONS[reason]}.`] : [];
362
367
  case "DOMAIN_PROFILE_AMBIGUOUS": return [domainAmbiguous("repeat")];
363
368
  case "OWNER_TOKEN_REVOKED":
@@ -491,8 +496,8 @@ const SITE_NAMESPACE_DESCRIPTION = PUBLIC_NAMESPACE_LEAD + "A site always gets i
491
496
  /** `namespace` for publish_json and publish_kv: data is never listed. */
492
497
  const DATA_NAMESPACE_DESCRIPTION = "Omit (default): private. namespace=public: the shared public namespace. JSON and KV are never listed in the gallery " +
493
498
  "or indexed, in any namespace; anyone who has the link can open it. " + NAMESPACE_FORMAT;
494
- const TTL_SCHEMA = z.union([z.number().positive(), z.string().min(1).max(32)]).optional().describe("Requested TTL: days as a number, or a string such as '2w', '36h' or '1 month' (default 15 days). " +
495
- "The server rounds to whole days and clamps to the allowed range (1-30; up to 90 with an owner token in a profile), reporting each adjustment in notices; the returned expiresAt is the actual expiry.");
499
+ const TTL_SCHEMA = z.union([z.number().positive(), z.string().min(1).max(32)]).optional().describe("TTL in days: up to 30 days without an account, up to 90 with a free profile (pass an ownerToken held in the profile); " +
500
+ "a default applies when omitted. Accepts whole days: 15, '15d', '15 days'. The returned expiresAt is the actual expiry.");
496
501
  const DELIVERY_MODE_SCHEMA = z.enum(["path", "subdomain"]).optional().describe("Optional delivery choice for a fresh publication, independent of password/namespace. " +
497
502
  "Fresh sites and HTML-format pages outside public (including the default unlisted namespace) always use subdomain; an explicit path there still publishes, on subdomain, with a delivery notice. " +
498
503
  "Everything else in any namespace (Markdown, code/text, files, JSON, KV, public HTML) defaults to path (Promotion); public HTML may automatically choose subdomain for browser features. " +
@@ -594,7 +599,7 @@ export function createServer(options = {}) {
594
599
  const apiFetch = options.fetch ?? fetch;
595
600
  const server = new McpServer({
596
601
  name: "brewpage-mcp",
597
- version: "1.22.0",
602
+ version: "1.23.0",
598
603
  }, { instructions: SERVER_INSTRUCTIONS });
599
604
  /** Caller argument first, then the configured default; `fromEnv` marks the configured one. */
600
605
  function ownerTokenFor(argument) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brewpage-mcp",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "mcpName": "io.github.kochetkov-ma/brewpage-mcp",
5
5
  "description": "MCP server for BrewPage — publish and manage HTML, sites, KV, JSON and files from AI assistants",
6
6
  "type": "module",