brewpage-mcp 1.21.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 +44 -24
- package/dist/guides/custom-domain.d.ts +4 -0
- package/dist/guides/custom-domain.js +26 -0
- package/dist/guides/page-data.d.ts +1 -1
- package/dist/guides/page-data.js +43 -48
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +185 -82
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
127
|
+
"args": ["-y", "brewpage-mcp@1.23.0"]
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
```
|
|
@@ -156,21 +156,21 @@ Thirty-two tools are available. Creation returns an owner token; keep it to modi
|
|
|
156
156
|
| `bind_token` | Add an owner token to your profile (admin token) |
|
|
157
157
|
| `mint_token` | Mint a profile token, shown once (admin token) |
|
|
158
158
|
| `set_domain` | Connect a domain you own to an HTML page or site; returns the DNS records to create |
|
|
159
|
-
| `list_domains` |
|
|
160
|
-
| `check_domain` |
|
|
159
|
+
| `list_domains` | Read one custom domain's status and DNS records without a check, or list the profile's custom domains (admin token) |
|
|
160
|
+
| `check_domain` | Run a DNS check once the records exist; status `pending_dns` -> `verifying` -> `active` |
|
|
161
161
|
| `remove_domain` | Remove a custom domain and free its slot; the BrewPage link keeps working |
|
|
162
162
|
| `get_page_data` | Read what visitors saved on your page: the settings and usage, or one list's entries |
|
|
163
163
|
| `set_page_data` | Write author entries (for example today's menu) without republishing the page |
|
|
164
164
|
| `configure_page_data` | Change who can read a list and its limits; only what you name changes |
|
|
165
165
|
| `delete_page_data` | Delete entries by id, or every entry in a time range |
|
|
166
|
-
| `get_guide` | Read
|
|
166
|
+
| `get_guide` | Read a short guide: Page data before writing a page that saves data, or the custom-domain steps |
|
|
167
167
|
| `search_gallery` | Browse public content or list owned resources |
|
|
168
168
|
| `get_stats` | Read platform-wide statistics |
|
|
169
169
|
| `get_limits` | Read the current upload limits (`GET /api/limits`) |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -510,37 +510,48 @@ Parameters: `label` (optional), `scope` (`publish` default | `admin`), `expiresI
|
|
|
510
510
|
|
|
511
511
|
### Custom domains
|
|
512
512
|
|
|
513
|
-
Show one HTML page (format html, no password) or one site on a domain you own, such as `www.example.com` or `example.com`. Free, 5 per profile.
|
|
513
|
+
Show one HTML page (format html, no password) or one site on a domain you own, such as `www.example.com` or `example.com`. Free, up to 5 per profile. A profile token works at once; an owner token must first be added to a free BrewPage profile: sign in at https://account.brewpage.app/tokens, press Add existing token and paste it (or `bind_token` with an admin profile token). Any registrar or DNS host works; if you are buying a domain, Cloudflare and Porkbun are the quickest to set up. The BrewPage link keeps working.
|
|
514
514
|
|
|
515
|
-
How it goes:
|
|
515
|
+
How it goes:
|
|
516
516
|
|
|
517
|
-
`
|
|
517
|
+
1. `set_domain` returns the DNS records. They come from the result, never from memory: a subdomain takes a TXT record at `_brewpage.<host>` plus a CNAME to `domains.brewpage.app`; a root domain takes the TXT record plus the A record shown, or the optional ALIAS row.
|
|
518
|
+
2. The domain owner creates exactly those records at their DNS provider: MX (mail) records stay, an A, AAAA or CNAME record at the same name is replaced, and at Cloudflare Proxy status is set to DNS only.
|
|
519
|
+
3. After the records exist, `check_domain` once.
|
|
520
|
+
4. `list_domains` with the same `type`, `namespace` and `id` reads the status without a new check until it is `active`. BrewPage checks on its own (the first full check about 15 minutes after adding); the domain is usually live within 30 minutes and HTTPS is issued automatically. Another `check_domain` is needed only after a DNS change.
|
|
521
|
+
|
|
522
|
+
Statuses: `pending_dns` (records not seen yet), `verifying` (DNS found, HTTPS being issued), `active` (live), `lost` (the records broke after going live; the domain is still served until `graceEndsAt`: restore the records and check again). The `Problem:` line of a result names what blocks progress and its fix; a refusal names its next step. The same steps are in `get_guide(topic="custom-domain")` for the assistant.
|
|
523
|
+
|
|
524
|
+
`publish_html` and `publish_site` accept an optional `domain` to connect it while publishing. The publication is always created; the result prints a `CUSTOM DOMAIN` block with the records, or says the domain was not set, with the next step, below the notice that explains why.
|
|
518
525
|
|
|
519
526
|
### `set_domain`
|
|
520
527
|
|
|
521
528
|
Connect a domain to an HTML page or site (`PUT /api/publications/{html|site}/{ns}/{id}/domain`). A hostname already in the profile moves to this publication. A page on a BrewPage path address moves to its own subdomain, and its BrewPage link keeps working.
|
|
522
529
|
|
|
523
|
-
Parameters: `type` (`html` | `site`), `namespace`, `id`, `hostname` (
|
|
530
|
+
Parameters: `type` (`html` | `site`), `namespace`, `id`, `hostname` (a bare hostname such as `www.example.com` or `example.com`, no scheme or path), `ownerToken` (optional; a profile token, or the publication's owner token added to a profile).
|
|
524
531
|
|
|
525
532
|
```
|
|
526
533
|
set_domain(type="html", namespace="public", id="aBcDeFgHiJ", hostname="www.example.com")
|
|
527
534
|
```
|
|
528
535
|
|
|
529
|
-
Output: `Custom domain
|
|
536
|
+
Output: `Custom domain added. It is not live until the DNS records below exist.`, then `Domain:`, `Status:`, `Problem:` (what is still missing, with the fix), `Serving:`, `Shows:`, the `records[]` rows verbatim as JSON (`type`, `name`, `host`, `value`, `state`) and the next step. `host` is the name DNS panels ask for.
|
|
530
537
|
|
|
531
538
|
---
|
|
532
539
|
|
|
533
540
|
### `list_domains`
|
|
534
541
|
|
|
535
|
-
|
|
542
|
+
Read custom domains without running a DNS check. With `type`, `namespace` and `id` (`GET /api/publications/{html|site}/{ns}/{id}/domain`; a profile token or an owner token added to the profile) or with `hostname` (`GET /api/domains/{hostname}`; admin profile token): the status, problem, DNS records and next step of that one custom domain. Without an address (`GET /api/domains`; admin profile token): every custom domain of the profile, plus how many are used and allowed.
|
|
543
|
+
|
|
544
|
+
Parameters: optional `type` + `namespace` + `id`, or `hostname`; `ownerToken` (optional, default `BREWPAGE_OWNER_TOKEN`, local server only).
|
|
536
545
|
|
|
537
|
-
|
|
546
|
+
```
|
|
547
|
+
list_domains(type="html", namespace="public", id="aBcDeFgHiJ")
|
|
548
|
+
```
|
|
538
549
|
|
|
539
550
|
---
|
|
540
551
|
|
|
541
552
|
### `check_domain`
|
|
542
553
|
|
|
543
|
-
|
|
554
|
+
Run a DNS check now and return the updated status and records. One check after the records exist is enough: BrewPage keeps checking on its own, and `list_domains` reads the status without a check. By publication: `type`, `namespace`, `id` with a profile token or an owner token added to the profile (`POST /api/publications/{html|site}/{ns}/{id}/domain/check`). By hostname: `hostname` with an admin profile token (`POST /api/domains/{hostname}/check`). A repeat within seconds answers 429 and the result prints `Retry-After`.
|
|
544
555
|
|
|
545
556
|
Parameters: either `type` + `namespace` + `id`, or `hostname`; `ownerToken` (optional).
|
|
546
557
|
|
|
@@ -584,9 +595,9 @@ The four page-data tools below take `namespace` + `id` + `ownerToken`, or `manag
|
|
|
584
595
|
|
|
585
596
|
### `get_guide`
|
|
586
597
|
|
|
587
|
-
Read a short reference text. Topic `page-data`: how a page saves what visitors enter, with the script calls
|
|
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.
|
|
588
599
|
|
|
589
|
-
Parameters: `topic` (`page-data`).
|
|
600
|
+
Parameters: `topic` (`page-data` | `custom-domain`).
|
|
590
601
|
|
|
591
602
|
---
|
|
592
603
|
|
|
@@ -693,7 +704,8 @@ get_limits()
|
|
|
693
704
|
| "List my published pages" / "What did I publish?" | `search_gallery` | Set `mine=true` + `ownerToken` |
|
|
694
705
|
| "Browse the public BrewPage gallery" | `search_gallery` | Optional `q`, `sort=date\|views` |
|
|
695
706
|
| "Put this page on my own domain" | `set_domain` | Owner token in a free profile; returns the DNS records to create |
|
|
696
|
-
| "I added the DNS records
|
|
707
|
+
| "I added the DNS records" | `check_domain` | Once; status `pending_dns` -> `verifying` -> `active` |
|
|
708
|
+
| "Is my domain live yet?" | `list_domains` | With `type`, `namespace`, `id`: reads the status without a new check |
|
|
697
709
|
| "Which domains do I have?" | `list_domains` | Admin token of the profile |
|
|
698
710
|
| "Disconnect my domain" | `remove_domain` | The BrewPage link keeps working |
|
|
699
711
|
| "Make a page with a guestbook / form / poll that saves answers" | `get_guide`, then `publish_html` | Publish with `state`; give the user the page link and the management link |
|
|
@@ -757,6 +769,14 @@ Every publish response includes an **owner token** -- the only credential that a
|
|
|
757
769
|
|
|
758
770
|
## Changelog
|
|
759
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
|
+
|
|
776
|
+
## 1.22.0 -- 2026-11-12
|
|
777
|
+
|
|
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`.
|
|
779
|
+
|
|
760
780
|
## 1.21.0 -- 2026-11-12
|
|
761
781
|
|
|
762
782
|
- Remote mode only: optional `GET /metrics` with usage counts for the operator (calls per tool, Claude, ChatGPT or other clients, distinct clients per day), served only to the addresses in `BREWPAGE_METRICS_CIDRS`. No addresses, arguments or content are kept or shown. Tools, inputs and stdio are unchanged: thirty-two tools, contract 2.27.0. `User-Agent: brewpage-mcp/1.21.0`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Custom-domain statuses: the "Statuses (`status`):" table under "## Custom Domains" of llms-full.txt. */
|
|
2
|
+
export declare const DOMAIN_STATUSES: Record<string, string>;
|
|
3
|
+
/** Custom-domain problems and fixes: the "Problems (`problem`) and the fix:" table of the same section; rows with a <placeholder> key left out. */
|
|
4
|
+
export declare const DOMAIN_PROBLEMS: Record<string, string>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by scripts/sync-guide.mjs from brewpage-app frontend/src (llms-full.txt); do not hand-edit.
|
|
2
|
+
/** Custom-domain statuses: the "Statuses (`status`):" table under "## Custom Domains" of llms-full.txt. */
|
|
3
|
+
export const DOMAIN_STATUSES = {
|
|
4
|
+
"pending_dns": "DNS records not seen yet: create them; `problem` names the missing one",
|
|
5
|
+
"verifying": "DNS records found, HTTPS being issued: wait",
|
|
6
|
+
"active": "live at `url` (`serving: true`)",
|
|
7
|
+
"lost": "DNS records broke after going live; still served until `graceEndsAt`: restore them, then check",
|
|
8
|
+
};
|
|
9
|
+
/** Custom-domain problems and fixes: the "Problems (`problem`) and the fix:" table of the same section; rows with a <placeholder> key left out. */
|
|
10
|
+
export const DOMAIN_PROBLEMS = {
|
|
11
|
+
"DOMAIN_NOT_REGISTERED": "the domain does not exist yet: register it with a domain registrar, then create the records",
|
|
12
|
+
"TXT_MISSING": "create the TXT record of `records[]`",
|
|
13
|
+
"TXT_WRONG": "a `bp-verify=` TXT record has another value: replace it with the one in `records[]`",
|
|
14
|
+
"ROUTING_MISSING": "create the CNAME record (apex: the A or ALIAS record)",
|
|
15
|
+
"ROUTING_WRONG": "the name points elsewhere: replace its A or CNAME record (Cloudflare: DNS only)",
|
|
16
|
+
"FOREIGN_AAAA": "delete the AAAA record listed in `records[]`",
|
|
17
|
+
"CAA_BLOCKS": "add the CAA record listed in `records[]`",
|
|
18
|
+
"DOMAIN_TAKEN": "this hostname is not available: use another hostname or write to info@brewpage.app",
|
|
19
|
+
"QUEUE_FULL": "DNS records are right, verification waits its turn: nothing to do",
|
|
20
|
+
"CERT_PENDING": "HTTPS certificate on its way: nothing to do",
|
|
21
|
+
"CERT_FAILED": "the certificate could not be issued: check the DNS records, then check again",
|
|
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, records not right a week after adding: fix DNS, then check again (resumes them)",
|
|
24
|
+
"RESOLVER_UNAVAILABLE": "DNS did not answer: retried automatically",
|
|
25
|
+
"PUBLICATION_GONE": "the publication is gone: connect the custom domain to another one",
|
|
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})";
|
package/dist/guides/page-data.js
CHANGED
|
@@ -1,53 +1,48 @@
|
|
|
1
|
-
// Generated by scripts/sync-guide.mjs from brewpage-app frontend/src (llms-full.txt
|
|
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
|
|
5
|
-
"1)
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"Errors
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
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.
|
|
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.
|
|
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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { DOMAIN_PROBLEMS, DOMAIN_STATUSES } from "./guides/custom-domain.js";
|
|
3
4
|
import { PAGE_DATA_CALLS, PAGE_DATA_GUIDE } from "./guides/page-data.js";
|
|
4
5
|
export const DEFAULT_BASE_URL = "https://brewpage.app";
|
|
5
|
-
export const USER_AGENT = "brewpage-mcp/1.
|
|
6
|
+
export const USER_AGENT = "brewpage-mcp/1.23.0";
|
|
6
7
|
export const OWNER_TOKEN_ENV = "BREWPAGE_OWNER_TOKEN";
|
|
7
8
|
/** Server instructions of the handshake: one paragraph per line; the first stands alone within 512 chars. */
|
|
8
9
|
export const SERVER_INSTRUCTIONS = [
|
|
@@ -10,6 +11,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
10
11
|
"After publishing, give the user the page link and the management link from the result. Say that the management link is private: whoever has it can edit or delete the page, and its Data tab shows what visitors saved.",
|
|
11
12
|
"KV and JSON tools hold your own data. Page data holds what a page's visitors save: get_page_data reads it, set_page_data writes author entries, configure_page_data changes who can read a list, delete_page_data moderates. In a later chat these four take the management link as manageUrl.",
|
|
12
13
|
"HTML pages and sites show a small BrewPage badge; page code must not hide or alter it. A profile can turn it off on up to 5 pages (set_badge).",
|
|
14
|
+
"A custom domain, only when the user asks for their own domain: set_domain, the user creates the returned DNS records at their DNS provider, check_domain once, then list_domains reads the status until it is active. Steps and fixes: get_guide topic custom-domain.",
|
|
13
15
|
].join("\n");
|
|
14
16
|
const READ_ONLY = { readOnlyHint: true };
|
|
15
17
|
const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
|
|
@@ -198,32 +200,67 @@ function configuredListLine(name, named, before, after) {
|
|
|
198
200
|
return after.mode === "shared" ? `${name} - ${listRuleText(after)} (was private)`
|
|
199
201
|
: `${name} - ${listRuleText(after)} (was shared; it cannot become shared again while it holds visitors' entries)`;
|
|
200
202
|
}
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
203
|
+
const DOMAIN_TOKENS_PAGE = "https://account.brewpage.app/tokens";
|
|
204
|
+
const DOMAIN_PROFILE_TEXT = `Free with a BrewPage profile, up to 5 custom domains: a profile token works at once, an owner token after it is added to a profile (sign-in at ${DOMAIN_TOKENS_PAGE}, "Add existing token").`;
|
|
205
|
+
const DOMAIN_PROVIDER_TEXT = "Any registrar or DNS host works; if the user is buying a domain, Cloudflare and Porkbun are the quickest to set up.";
|
|
206
|
+
const DOMAIN_ADDRESS_TEXT = "Addressed by publication (type, namespace, id; a profile token or an owner token added to the profile) or by hostname (an admin profile token).";
|
|
207
|
+
const DOMAIN_OWNER_TOKEN = `A profile token, or the publication's owner token added to a BrewPage profile. Default: ${ENV_DEFAULT}.`;
|
|
208
|
+
const DOMAIN_EITHER_TOKEN = `By publication: a profile token or an owner token added to the profile. By hostname: an admin profile token. Default: ${ENV_DEFAULT}.`;
|
|
209
|
+
const DOMAIN_LIST_TOKEN = `Whole list or by hostname: an admin profile token. By publication: a profile token or an owner token added to the profile. Default: ${ENV_DEFAULT}.`;
|
|
210
|
+
const DOMAIN_ADDRESS_ERROR = "Provide either `hostname` (an admin profile token) or `type`, `namespace` and `id` (a profile token or an owner token added to the profile).";
|
|
211
|
+
const DOMAIN_HOSTNAME = "Custom domain as a bare hostname, e.g. www.example.com; instead of type, namespace and id";
|
|
212
|
+
const DOMAIN_ARGUMENT = "Optional custom domain for the new publication: a bare hostname the user owns (www.example.com or example.com, no scheme or path). " +
|
|
213
|
+
"Needs a profile token or an owner token added to a free BrewPage profile, and no password. The publication is always created; " +
|
|
214
|
+
"the result lists the DNS records for the domain owner to create, or says why the custom domain was not set.";
|
|
215
|
+
/** Status sentence of the check_domain description; names every key of DOMAIN_STATUSES. */
|
|
216
|
+
const DOMAIN_STATUS_TEXT = "Status: pending_dns (DNS records not seen yet), verifying (DNS records found, HTTPS being issued), active (live), " +
|
|
217
|
+
"lost (DNS records broke after going live; still served until `graceEndsAt`; restored records and one more check bring it back). `problem` names the fix.";
|
|
218
|
+
/** Next step per `reason` of 409 NOT_ELIGIBLE and per `NOT_ELIGIBLE_<reason>` problem. */
|
|
219
|
+
const DOMAIN_REASONS = {
|
|
220
|
+
OWNERSHIP: `add its owner token to the profile (bind_token with an admin profile token, or "Add existing token" at ${DOMAIN_TOKENS_PAGE})`,
|
|
221
|
+
OLD: "it is an old publication: publish it again and connect the custom domain to the new one",
|
|
222
|
+
RETIRED: "its address was retired: connect the custom domain to a live publication",
|
|
223
|
+
TYPE: "only HTML pages and sites can",
|
|
224
|
+
FORMAT: "the page is not format html: publish it as format html",
|
|
225
|
+
PASSWORD: "remove the password (management link, or POST /api/{html|sites}/{ns}/{id}/password {\"password\":null})",
|
|
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",
|
|
225
228
|
};
|
|
226
|
-
const
|
|
229
|
+
const DOMAIN_READ_STATUS = "read the status with list_domains (same type, namespace and id, or hostname)";
|
|
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). " +
|
|
231
|
+
"MX/mail records stay; an A, AAAA or CNAME record at the same name is replaced. When the user says the records exist, call check_domain once, then " +
|
|
232
|
+
DOMAIN_READ_STATUS + " until it is active: BrewPage checks on its own (first full check about 15 minutes after adding), " +
|
|
233
|
+
"usually live within 30 minutes, HTTPS is automatic. Call check_domain again only after a DNS change.";
|
|
234
|
+
const DOMAIN_NEXT_VERIFYING = "Next: DNS records are found and HTTPS is being issued. To follow it, " + DOMAIN_READ_STATUS +
|
|
235
|
+
" until it is active; call check_domain again only if the Problem line says so or DNS changed.";
|
|
236
|
+
const DOMAIN_NEXT_LOST = "Next: DNS records broke after going live. Ask the user to restore exactly these DNS records at their DNS provider, then call check_domain once.";
|
|
237
|
+
const DOMAIN_SUSPENDED_TEXT = "BrewPage suspended this custom domain: the user writes to info@brewpage.app; repeating a call does not help.";
|
|
238
|
+
/** Custom-domain guide: the flow in tool words, then the status and problem tables generated from llms-full.txt. */
|
|
239
|
+
const CUSTOM_DOMAIN_GUIDE = [
|
|
240
|
+
"BrewPage custom domain: a hostname the user owns (example.com, docs.example.com) shows one HTML page of format html or one site, without a password. Only when the user asks for their own domain.",
|
|
241
|
+
`Who: ${DOMAIN_PROFILE_TEXT}`,
|
|
242
|
+
" 403 DOMAIN_NEEDS_ACCOUNT = the owner token is in no profile: give the user the owner token (it is theirs) to add there, then repeat the call. With an admin profile token bind_token adds it instead.",
|
|
243
|
+
`Domain: ${DOMAIN_PROVIDER_TEXT}`,
|
|
244
|
+
"Steps:",
|
|
245
|
+
"1) set_domain (type, namespace, id, hostname), or `domain` on publish_html / publish_site. The hostname is bare: www.example.com or example.com, no scheme or path.",
|
|
246
|
+
"2) Give the user exactly the DNS records of the result to create at their DNS provider (`host` is the name DNS panels ask for); never values from memory.",
|
|
247
|
+
" MX/mail records stay; an A, AAAA or CNAME record at the same name is replaced. At Cloudflare set Proxy status to DNS only.",
|
|
248
|
+
"3) When the user says the records exist: check_domain once.",
|
|
249
|
+
"4) Read the status with list_domains (type, namespace, id) until it is active. BrewPage checks on its own (first full check about 15 minutes after adding);",
|
|
250
|
+
" usually live within 30 minutes, HTTPS is issued automatically. check_domain again only after a DNS change; on 429 wait Retry-After and read instead.",
|
|
251
|
+
"Statuses (`Status:` line of a result):",
|
|
252
|
+
...Object.entries(DOMAIN_STATUSES).map(([status, text]) => ` ${status} - ${text}`),
|
|
253
|
+
"Problems (`Problem:` line) and the fix:",
|
|
254
|
+
...Object.entries(DOMAIN_PROBLEMS).map(([problem, fix]) => ` ${problem} - ${fix}`),
|
|
255
|
+
" NOT_ELIGIBLE_<reason> - the publication cannot take a custom domain now; the line names the fix",
|
|
256
|
+
"A refusal names its next step. The BrewPage link keeps working throughout. set_domain with the same hostname on another publication of the profile moves it; remove_domain removes it.",
|
|
257
|
+
].join("\n") + "\n";
|
|
258
|
+
/** `Problem:` value: the code plus its fix (generated table, or the reason of a `NOT_ELIGIBLE_<reason>` code); other codes print bare. */
|
|
259
|
+
function domainProblemText(problem) {
|
|
260
|
+
const reason = problem.startsWith("NOT_ELIGIBLE_") ? DOMAIN_REASONS[problem.slice("NOT_ELIGIBLE_".length)] : undefined;
|
|
261
|
+
const fix = reason ? `the publication cannot take a custom domain now: ${reason}` : DOMAIN_PROBLEMS[problem];
|
|
262
|
+
return fix ? `${problem} (${fix})` : problem;
|
|
263
|
+
}
|
|
227
264
|
/** One custom domain (`DomainItem`): status, `records[]` verbatim and the next step. */
|
|
228
265
|
function domainLines(item) {
|
|
229
266
|
const { hostname, displayHostname, problem, url } = item;
|
|
@@ -233,7 +270,7 @@ function domainLines(item) {
|
|
|
233
270
|
`Status: ${item.status}`,
|
|
234
271
|
];
|
|
235
272
|
if (typeof problem === "string" && problem)
|
|
236
|
-
lines.push(`Problem: ${
|
|
273
|
+
lines.push(`Problem: ${domainProblemText(problem)}`);
|
|
237
274
|
lines.push(`Serving: ${item.serving === true}`);
|
|
238
275
|
if (item.suspended === true)
|
|
239
276
|
lines.push(`suspended: true`);
|
|
@@ -247,7 +284,7 @@ function domainLines(item) {
|
|
|
247
284
|
lines.push(`The publication moved to its own BrewPage subdomain (a custom domain needs it); its BrewPage link keeps working.`);
|
|
248
285
|
if (typeof item.detached === "string" && item.detached)
|
|
249
286
|
lines.push(`Earlier domain ${item.detached} stays in the profile, not attached.`);
|
|
250
|
-
for (const [field, label] of [["checkedAt", "Checked"], ["nextCheckAt", "Next automatic check"], ["graceEndsAt", "
|
|
287
|
+
for (const [field, label] of [["checkedAt", "Checked"], ["nextCheckAt", "Next automatic check"], ["graceEndsAt", "Served until (graceEndsAt)"]]) {
|
|
251
288
|
if (typeof item[field] === "string")
|
|
252
289
|
lines.push(`${label}: ${item[field]}`);
|
|
253
290
|
}
|
|
@@ -259,44 +296,88 @@ function domainLines(item) {
|
|
|
259
296
|
if (item.provider === "cloudflare")
|
|
260
297
|
lines.push(`Cloudflare: Proxy status "DNS only" on the CNAME or A record.`);
|
|
261
298
|
if (item.suspended === true)
|
|
262
|
-
return lines;
|
|
299
|
+
return [...lines, DOMAIN_SUSPENDED_TEXT];
|
|
300
|
+
if (problem === "DOMAIN_NOT_REGISTERED")
|
|
301
|
+
lines.push(DOMAIN_PROVIDER_TEXT);
|
|
302
|
+
// The DNS provider is unknown until a check names it, so the Cloudflare rule travels with the step.
|
|
263
303
|
if (item.status === "pending_dns")
|
|
264
|
-
lines.push(DOMAIN_NEXT_CREATE);
|
|
304
|
+
lines.push(DOMAIN_NEXT_CREATE + (typeof item.provider === "string" && item.provider ? "" : " At Cloudflare set Proxy status to DNS only."));
|
|
265
305
|
if (item.status === "verifying")
|
|
266
|
-
lines.push(
|
|
306
|
+
lines.push(DOMAIN_NEXT_VERIFYING);
|
|
267
307
|
if (item.status === "active")
|
|
268
|
-
lines.push(`Live: the domain shows the publication over HTTPS. The BrewPage link keeps working.`);
|
|
308
|
+
lines.push(`Live: the custom domain shows the publication over HTTPS. The BrewPage link keeps working.`);
|
|
269
309
|
if (item.status === "lost")
|
|
270
|
-
lines.push(
|
|
310
|
+
lines.push(DOMAIN_NEXT_LOST);
|
|
271
311
|
return lines;
|
|
272
312
|
}
|
|
273
|
-
/**
|
|
274
|
-
function
|
|
313
|
+
/** 403 `DOMAIN_NEEDS_ACCOUNT` and its create notice: the profile step in the user's hands, then `then`. */
|
|
314
|
+
function domainNeedsProfile(fromEnv, then) {
|
|
315
|
+
return `The owner token is in no BrewPage profile. Give the user the owner token (it is theirs${fromEnv ? `; it is the ${OWNER_TOKEN_ENV} value in this server's settings` : ""}) ` +
|
|
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
|
+
}
|
|
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;";
|
|
321
|
+
/** 409 `DOMAIN_PROFILE_AMBIGUOUS` and its create notice; `then` = the call to make with the profile token. */
|
|
322
|
+
function domainAmbiguous(then) {
|
|
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}).`;
|
|
324
|
+
}
|
|
325
|
+
/** Next step per `domain-*` notice of a create; other codes point at the notice text. */
|
|
326
|
+
function publishDomainNext(code, fromEnv) {
|
|
327
|
+
if (code === "domain-needs-account")
|
|
328
|
+
return ` ${domainNeedsProfile(fromEnv, "call set_domain for this publication")}`;
|
|
329
|
+
if (code === "domain-limit-reached")
|
|
330
|
+
return ` Next: ${DOMAIN_LIMIT_NEXT}`;
|
|
331
|
+
if (code === "domain-profile-ambiguous")
|
|
332
|
+
return ` ${domainAmbiguous("call set_domain")}`;
|
|
333
|
+
if (code === "domain-not-eligible" || code === "domain-invalid")
|
|
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.`;
|
|
337
|
+
if (code === "domain-account-restricted")
|
|
338
|
+
return " The domain notice above says why.";
|
|
339
|
+
return " The domain notice above says why. set_domain connects it later.";
|
|
340
|
+
}
|
|
341
|
+
/** Create with `domain`: the returned `customDomain`, else a line saying it was not set plus the next step of its `domain-*` notice. */
|
|
342
|
+
function publishDomainLines(data, requested, fromEnv = false) {
|
|
275
343
|
const item = data.customDomain;
|
|
276
344
|
if (item && typeof item === "object")
|
|
277
345
|
return [``, `CUSTOM DOMAIN`, ...domainLines(item)];
|
|
278
346
|
if (!requested)
|
|
279
347
|
return [];
|
|
280
|
-
const
|
|
281
|
-
return [``, `CUSTOM DOMAIN: ${requested} was not set; the publication is live at its BrewPage link
|
|
348
|
+
const notice = notices(data).find((n) => n.code.startsWith("domain-"));
|
|
349
|
+
return [``, `CUSTOM DOMAIN: ${requested} was not set; the publication is live at its BrewPage link.` +
|
|
350
|
+
(notice ? publishDomainNext(notice.code, fromEnv) : " set_domain connects it later.")];
|
|
282
351
|
}
|
|
283
|
-
/** Next step after a custom-domain refusal;
|
|
284
|
-
function domainHints(data,
|
|
285
|
-
const { code, limit, used } = (data ?? {});
|
|
286
|
-
|
|
287
|
-
|
|
352
|
+
/** Next step after a custom-domain refusal; `address` = how the call named the domain. Other codes keep the server answer alone. */
|
|
353
|
+
function domainHints(data, address, fromEnv = false) {
|
|
354
|
+
const { code, limit, used, reason } = (data ?? {});
|
|
355
|
+
switch (code) {
|
|
356
|
+
case "DOMAIN_NEEDS_ACCOUNT": return [domainNeedsProfile(fromEnv, "repeat this call")];
|
|
357
|
+
case "DOMAIN_LIMIT": return typeof limit === "number" && limit > 0
|
|
358
|
+
? [`The profile uses ${typeof used === "number" ? used : limit} of ${limit} custom domains: ${DOMAIN_LIMIT_NEXT}`] : [];
|
|
359
|
+
case "ADMIN_TOKEN_REQUIRED": return ["This call needs an admin profile token." + (address === "publication" ? ""
|
|
360
|
+
: " With a publish-scope profile token or an owner token added to the profile, pass type, namespace and id" + (address === "hostname" ? " instead of hostname." : "."))];
|
|
361
|
+
case "DOMAIN_CHECK_TOO_SOON": return [`Checked a moment ago: do not call check_domain again now. BrewPage keeps checking on its own; after Retry-After ${DOMAIN_READ_STATUS}.`];
|
|
362
|
+
case "DOMAIN_NOT_FOUND": return ["No custom domain there: list_domains shows the profile's custom domains; set_domain connects one."];
|
|
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."];
|
|
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.`];
|
|
366
|
+
case "NOT_ELIGIBLE": return typeof reason === "string" && DOMAIN_REASONS[reason] ? [`This publication cannot take a custom domain: ${DOMAIN_REASONS[reason]}.`] : [];
|
|
367
|
+
case "DOMAIN_PROFILE_AMBIGUOUS": return [domainAmbiguous("repeat")];
|
|
368
|
+
case "OWNER_TOKEN_REVOKED":
|
|
369
|
+
case "OWNER_TOKEN_EXPIRED":
|
|
370
|
+
return [`The token ${code === "OWNER_TOKEN_REVOKED" ? "was revoked" : "expired"}: repeat with another profile token or the publication's owner token.`];
|
|
371
|
+
case "ACCOUNT_RESTRICTED":
|
|
372
|
+
case "ACCOUNT_BLOCKED":
|
|
373
|
+
return [`This profile cannot add custom domains${code === "ACCOUNT_RESTRICTED" ? " now" : ""}: tell the user; repeating the call does not help.`];
|
|
374
|
+
case "DOMAIN_SUSPENDED": return [DOMAIN_SUSPENDED_TEXT];
|
|
375
|
+
case "PUBLICATION_UNAVAILABLE": return ["The publication is missing or expired (outside public a wrong token answers the same): check type, namespace, id and the token."];
|
|
376
|
+
case "FEATURE_DISABLED":
|
|
377
|
+
case "DOMAIN_INTAKE_PAUSED":
|
|
378
|
+
case "HOSTING_CREATION_PAUSED": return ["Temporary: tell the user and try again later."];
|
|
379
|
+
default: return [];
|
|
288
380
|
}
|
|
289
|
-
if (code === "DOMAIN_LIMIT" && typeof limit === "number" && limit > 0) {
|
|
290
|
-
return [`The profile uses ${typeof used === "number" ? used : limit} of ${limit} custom domains: remove_domain frees one; a hostname already in the profile moves here with set_domain.`];
|
|
291
|
-
}
|
|
292
|
-
if (code === "ADMIN_TOKEN_REQUIRED") {
|
|
293
|
-
return ["Custom domains need an admin token from a free BrewPage profile." + (byHostname ? " With an owner token of the publication, pass type, namespace and id instead of hostname." : "")];
|
|
294
|
-
}
|
|
295
|
-
if (code === "DOMAIN_CHECK_TOO_SOON")
|
|
296
|
-
return ["Checked a moment ago: wait, then call check_domain again."];
|
|
297
|
-
if (code === "DOMAIN_NOT_FOUND")
|
|
298
|
-
return ["No custom domain there: list_domains shows the profile's domains; set_domain connects one."];
|
|
299
|
-
return [];
|
|
300
381
|
}
|
|
301
382
|
/** `/api/<segments>` with each segment encoded; empty, `.` and `..` refused (URL parsing would resolve them away). */
|
|
302
383
|
export function apiPath(...segments) {
|
|
@@ -415,8 +496,8 @@ const SITE_NAMESPACE_DESCRIPTION = PUBLIC_NAMESPACE_LEAD + "A site always gets i
|
|
|
415
496
|
/** `namespace` for publish_json and publish_kv: data is never listed. */
|
|
416
497
|
const DATA_NAMESPACE_DESCRIPTION = "Omit (default): private. namespace=public: the shared public namespace. JSON and KV are never listed in the gallery " +
|
|
417
498
|
"or indexed, in any namespace; anyone who has the link can open it. " + NAMESPACE_FORMAT;
|
|
418
|
-
const TTL_SCHEMA = z.union([z.number().positive(), z.string().min(1).max(32)]).optional().describe("
|
|
419
|
-
"
|
|
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.");
|
|
420
501
|
const DELIVERY_MODE_SCHEMA = z.enum(["path", "subdomain"]).optional().describe("Optional delivery choice for a fresh publication, independent of password/namespace. " +
|
|
421
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. " +
|
|
422
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. " +
|
|
@@ -518,7 +599,7 @@ export function createServer(options = {}) {
|
|
|
518
599
|
const apiFetch = options.fetch ?? fetch;
|
|
519
600
|
const server = new McpServer({
|
|
520
601
|
name: "brewpage-mcp",
|
|
521
|
-
version: "1.
|
|
602
|
+
version: "1.23.0",
|
|
522
603
|
}, { instructions: SERVER_INSTRUCTIONS });
|
|
523
604
|
/** Caller argument first, then the configured default; `fromEnv` marks the configured one. */
|
|
524
605
|
function ownerTokenFor(argument) {
|
|
@@ -584,7 +665,7 @@ export function createServer(options = {}) {
|
|
|
584
665
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
585
666
|
lines.push(...hostingResponseLines(data));
|
|
586
667
|
lines.push(...pageDataPublishLines(data, sentState));
|
|
587
|
-
lines.push(...publishDomainLines(data, requestedDomain));
|
|
668
|
+
lines.push(...publishDomainLines(data, requestedDomain, sentOwnerToken !== undefined && sentOwnerToken === defaultOwnerToken));
|
|
588
669
|
lines.push(...ownerTokenLines(data, "update or delete this resource later", sentOwnerToken));
|
|
589
670
|
lines.push(...ownerLinkLines(data));
|
|
590
671
|
const ns = (data.namespace ?? effectiveNamespace) || "";
|
|
@@ -599,7 +680,7 @@ export function createServer(options = {}) {
|
|
|
599
680
|
"Format order: `format`, then a known filename extension, else the server detects it. Returns its primary share link and owner token. Optional password, TTL, filename, top toolbar (showTopBar). " +
|
|
600
681
|
"A `public` page without deliveryMode may get its own subdomain if it needs browser features a BrewPage link lacks; " +
|
|
601
682
|
"the result then has deliveryModeReason and a delivery notice. Markdown raw HTML is allowlisted: scripts, styles, event handlers, forms, embeds removed. " +
|
|
602
|
-
"Optional `domain` connects a domain the user owns to an HTML-format page. " +
|
|
683
|
+
"Optional `domain` connects a custom domain the user owns to an HTML-format page. " +
|
|
603
684
|
MANAGE_URL_TEXT + " " + SECRETS + " " + BADGE_TEXT + " " + PUBLISH_STATE_TEXT,
|
|
604
685
|
inputSchema: {
|
|
605
686
|
content: z.string().describe("HTML, Markdown, code or JSON to publish"),
|
|
@@ -863,7 +944,7 @@ export function createServer(options = {}) {
|
|
|
863
944
|
...toolMeta("Publish site", ADDITIVE),
|
|
864
945
|
description: "Publish an HTML site to BrewPage. " + PUBLIC_HINT + "Single-page: `entryContent`. Multi-page: `files` (`{path, content}` array). Optional `entry` overrides `index.html`. Returns its primary share link and owner token. " +
|
|
865
946
|
"Static only: HTML, CSS, JS (incl. built React/Vue bundles) run in the browser; server code (PHP etc.) never runs. " + SITE_LIMITS_TEXT +
|
|
866
|
-
" Optional `domain` connects a domain the user owns. " + MANAGE_URL_TEXT + " " + SECRETS + " " + BADGE_TEXT + " " + PUBLISH_STATE_TEXT,
|
|
947
|
+
" Optional `domain` connects a custom domain the user owns. " + MANAGE_URL_TEXT + " " + SECRETS + " " + BADGE_TEXT + " " + PUBLISH_STATE_TEXT,
|
|
867
948
|
inputSchema: {
|
|
868
949
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
869
950
|
entryContent: z
|
|
@@ -969,7 +1050,7 @@ export function createServer(options = {}) {
|
|
|
969
1050
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
970
1051
|
lines.push(...hostingResponseLines(site));
|
|
971
1052
|
lines.push(...pageDataPublishLines(site, sentState?.value));
|
|
972
|
-
lines.push(...publishDomainLines(site, domain || undefined));
|
|
1053
|
+
lines.push(...publishDomainLines(site, domain || undefined, owner !== undefined && owner === defaultOwnerToken));
|
|
973
1054
|
lines.push(...ownerTokenLines(site, "view info or delete this site", owner));
|
|
974
1055
|
lines.push(...ownerLinkLines(site));
|
|
975
1056
|
const ns = (site.namespace || effectiveNamespace) || "";
|
|
@@ -1636,15 +1717,18 @@ export function createServer(options = {}) {
|
|
|
1636
1717
|
}
|
|
1637
1718
|
server.registerTool("set_domain", {
|
|
1638
1719
|
...toolMeta("Connect custom domain", ADDITIVE),
|
|
1639
|
-
description: "Connect a domain the user owns
|
|
1640
|
-
" Returns the DNS records for the domain owner to create at their DNS provider
|
|
1641
|
-
"A
|
|
1642
|
-
"
|
|
1720
|
+
description: "Connect a custom domain (a hostname the user owns) to one HTML page of format html or one site, without a password. " + DOMAIN_PROFILE_TEXT +
|
|
1721
|
+
" Returns the DNS records for the domain owner to create at their DNS provider exactly as returned: MX/mail records stay, " +
|
|
1722
|
+
"an A, AAAA or CNAME record at the same name is replaced, at Cloudflare Proxy status is DNS only. " +
|
|
1723
|
+
"One DNS check after the records exist is enough: BrewPage keeps checking on its own (first full check about 15 minutes after adding); " +
|
|
1724
|
+
"usually live within 30 minutes, HTTPS is automatic. Status: pending_dns -> verifying -> active (lost when the DNS records break later); `problem` names the fix. " +
|
|
1725
|
+
"A hostname already in the profile moves to this publication; the BrewPage link keeps working. " + DOMAIN_PROVIDER_TEXT +
|
|
1726
|
+
" Steps and fixes: guide topic `custom-domain`.",
|
|
1643
1727
|
inputSchema: {
|
|
1644
1728
|
type: z.enum(["html", "site"]).describe("Publication type"),
|
|
1645
1729
|
namespace: z.string().describe("Publication namespace"),
|
|
1646
1730
|
id: z.string().describe("Publication ID"),
|
|
1647
|
-
hostname: z.string().describe("
|
|
1731
|
+
hostname: z.string().describe("Bare hostname the user owns: www.example.com or example.com, no scheme or path"),
|
|
1648
1732
|
ownerToken: z.string().optional().describe(DOMAIN_OWNER_TOKEN),
|
|
1649
1733
|
},
|
|
1650
1734
|
}, async ({ type, namespace, id, hostname, ownerToken }) => {
|
|
@@ -1654,23 +1738,40 @@ export function createServer(options = {}) {
|
|
|
1654
1738
|
const path = apiPath("publications", type, namespace, id, "domain");
|
|
1655
1739
|
const { ok, status, data, headers: answerHeaders } = await apiRequest("PUT", path, { hostname }, { "X-Owner-Token": auth.token });
|
|
1656
1740
|
if (!ok)
|
|
1657
|
-
return ownerFailure("set domain", status, data, auth.fromEnv, answerHeaders, domainHints(data,
|
|
1658
|
-
const lead = status === 201 ? "Custom domain
|
|
1741
|
+
return ownerFailure("set domain", status, data, auth.fromEnv, answerHeaders, domainHints(data, "publication", auth.fromEnv));
|
|
1742
|
+
const lead = status === 201 ? "Custom domain added. It is not live until the DNS records below exist."
|
|
1743
|
+
: "Custom domain set: it was already in the profile (moved here or unchanged).";
|
|
1659
1744
|
return textResult([lead, ``, ...domainLines(data)].join("\n"));
|
|
1660
1745
|
});
|
|
1661
1746
|
server.registerTool("list_domains", {
|
|
1662
1747
|
...toolMeta("List custom domains", READ_ONLY),
|
|
1663
|
-
description: "
|
|
1748
|
+
description: "Read custom domains without running a DNS check: status, `problem`, DNS records and the publication shown. " +
|
|
1749
|
+
"No address: every custom domain of the profile plus how many it holds and may hold. With type, namespace and id, or with hostname: that one custom domain.",
|
|
1664
1750
|
inputSchema: {
|
|
1665
|
-
|
|
1751
|
+
type: z.enum(["html", "site"]).optional().describe("Publication type; with namespace and id, instead of hostname"),
|
|
1752
|
+
namespace: z.string().optional().describe("Publication namespace"),
|
|
1753
|
+
id: z.string().optional().describe("Publication ID"),
|
|
1754
|
+
hostname: z.string().optional().describe(DOMAIN_HOSTNAME),
|
|
1755
|
+
ownerToken: z.string().optional().describe(DOMAIN_LIST_TOKEN),
|
|
1666
1756
|
},
|
|
1667
|
-
}, async ({ ownerToken }) => {
|
|
1757
|
+
}, async ({ type, namespace, id, hostname, ownerToken }) => {
|
|
1668
1758
|
const auth = ownerTokenFor(ownerToken);
|
|
1669
1759
|
if (!auth.token)
|
|
1670
1760
|
return textResult(NO_OWNER_TOKEN, true);
|
|
1761
|
+
// An empty string reads as an omitted argument: the list stays reachable for a client that fills every field.
|
|
1762
|
+
const [kind, ns, row, host] = [type, namespace, id, hostname].map((part) => part || undefined);
|
|
1763
|
+
if ([kind, ns, row, host].some((part) => part !== undefined)) {
|
|
1764
|
+
const path = domainPath(kind, ns, row, host);
|
|
1765
|
+
if (path === null)
|
|
1766
|
+
return textResult(DOMAIN_ADDRESS_ERROR, true);
|
|
1767
|
+
const { ok, status, data, headers: answerHeaders } = await apiRequest("GET", path, undefined, { "X-Owner-Token": auth.token });
|
|
1768
|
+
if (!ok)
|
|
1769
|
+
return ownerFailure("read domain", status, data, auth.fromEnv, answerHeaders, domainHints(data, host !== undefined ? "hostname" : "publication", auth.fromEnv));
|
|
1770
|
+
return textResult(domainLines(data).join("\n"));
|
|
1771
|
+
}
|
|
1671
1772
|
const { ok, status, data, headers: answerHeaders } = await apiRequest("GET", "/api/domains", undefined, { "X-Owner-Token": auth.token });
|
|
1672
1773
|
if (!ok)
|
|
1673
|
-
return ownerFailure("list domains", status, data, auth.fromEnv, answerHeaders, domainHints(data,
|
|
1774
|
+
return ownerFailure("list domains", status, data, auth.fromEnv, answerHeaders, domainHints(data, "list", auth.fromEnv));
|
|
1674
1775
|
const list = data;
|
|
1675
1776
|
const items = Array.isArray(list.items) ? list.items : [];
|
|
1676
1777
|
const lines = [`Custom domains: ${list.used} of ${list.limit} used`];
|
|
@@ -1682,14 +1783,15 @@ export function createServer(options = {}) {
|
|
|
1682
1783
|
});
|
|
1683
1784
|
server.registerTool("check_domain", {
|
|
1684
1785
|
...toolMeta("Check custom domain DNS", ADDITIVE),
|
|
1685
|
-
description: "
|
|
1686
|
-
"
|
|
1687
|
-
"
|
|
1786
|
+
description: "Run a DNS check of a custom domain now and return its status, `problem` and DNS records. " +
|
|
1787
|
+
"One check after the DNS records exist is enough: BrewPage keeps checking on its own (`nextCheckAt`) and reading the status needs no check; " +
|
|
1788
|
+
"another check fits only after a DNS change. " + DOMAIN_STATUS_TEXT + " " + DOMAIN_ADDRESS_TEXT +
|
|
1789
|
+
" A repeat within seconds answers 429 with Retry-After.",
|
|
1688
1790
|
inputSchema: {
|
|
1689
1791
|
type: z.enum(["html", "site"]).optional().describe("Publication type; with namespace and id, instead of hostname"),
|
|
1690
1792
|
namespace: z.string().optional().describe("Publication namespace"),
|
|
1691
1793
|
id: z.string().optional().describe("Publication ID"),
|
|
1692
|
-
hostname: z.string().optional().describe(
|
|
1794
|
+
hostname: z.string().optional().describe(DOMAIN_HOSTNAME),
|
|
1693
1795
|
ownerToken: z.string().optional().describe(DOMAIN_EITHER_TOKEN),
|
|
1694
1796
|
},
|
|
1695
1797
|
}, async ({ type, namespace, id, hostname, ownerToken }) => {
|
|
@@ -1701,18 +1803,18 @@ export function createServer(options = {}) {
|
|
|
1701
1803
|
return textResult(DOMAIN_ADDRESS_ERROR, true);
|
|
1702
1804
|
const { ok, status, data, headers: answerHeaders } = await apiRequest("POST", path, undefined, { "X-Owner-Token": auth.token });
|
|
1703
1805
|
if (!ok)
|
|
1704
|
-
return ownerFailure("check domain", status, data, auth.fromEnv, answerHeaders, domainHints(data, hostname !== undefined));
|
|
1806
|
+
return ownerFailure("check domain", status, data, auth.fromEnv, answerHeaders, domainHints(data, hostname !== undefined ? "hostname" : "publication", auth.fromEnv));
|
|
1705
1807
|
return textResult([`DNS checked.`, ``, ...domainLines(data)].join("\n"));
|
|
1706
1808
|
});
|
|
1707
1809
|
server.registerTool("remove_domain", {
|
|
1708
1810
|
...toolMeta("Remove custom domain", DESTRUCTIVE),
|
|
1709
1811
|
description: "Remove a custom domain from the profile: it stops showing the publication and its slot is free at once. The BrewPage link keeps working. " +
|
|
1710
|
-
DOMAIN_ADDRESS_TEXT + " DNS records at the provider stay until the domain owner deletes them.",
|
|
1812
|
+
DOMAIN_ADDRESS_TEXT + " DNS records at the DNS provider stay until the domain owner deletes them.",
|
|
1711
1813
|
inputSchema: {
|
|
1712
1814
|
type: z.enum(["html", "site"]).optional().describe("Publication type; with namespace and id, instead of hostname"),
|
|
1713
1815
|
namespace: z.string().optional().describe("Publication namespace"),
|
|
1714
1816
|
id: z.string().optional().describe("Publication ID"),
|
|
1715
|
-
hostname: z.string().optional().describe(
|
|
1817
|
+
hostname: z.string().optional().describe(DOMAIN_HOSTNAME),
|
|
1716
1818
|
ownerToken: z.string().optional().describe(DOMAIN_EITHER_TOKEN),
|
|
1717
1819
|
},
|
|
1718
1820
|
}, async ({ type, namespace, id, hostname, ownerToken }) => {
|
|
@@ -1724,7 +1826,7 @@ export function createServer(options = {}) {
|
|
|
1724
1826
|
return textResult(DOMAIN_ADDRESS_ERROR, true);
|
|
1725
1827
|
const { ok, status, data, headers: answerHeaders } = await apiRequest("DELETE", path, undefined, { "X-Owner-Token": auth.token });
|
|
1726
1828
|
if (!ok)
|
|
1727
|
-
return ownerFailure("remove domain", status, data, auth.fromEnv, answerHeaders, domainHints(data, hostname !== undefined));
|
|
1829
|
+
return ownerFailure("remove domain", status, data, auth.fromEnv, answerHeaders, domainHints(data, hostname !== undefined ? "hostname" : "publication", auth.fromEnv));
|
|
1728
1830
|
const what = hostname !== undefined ? `Custom domain removed: ${hostname}` : `No custom domain on ${type} ${namespace}/${id} now`;
|
|
1729
1831
|
return textResult(`${what} · slot free · BrewPage link unchanged\nIts DNS records can be deleted at the DNS provider.`);
|
|
1730
1832
|
});
|
|
@@ -1933,10 +2035,11 @@ export function createServer(options = {}) {
|
|
|
1933
2035
|
...toolMeta("Read BrewPage guide", READ_ONLY),
|
|
1934
2036
|
description: "Short reference texts for building on BrewPage. Topic `page-data`: how a published page saves what visitors enter " +
|
|
1935
2037
|
"(forms, guestbook, votes, leaderboard, game progress) with the script `https://brewpage.app/js/state.js` and the `state` config; " +
|
|
1936
|
-
"meant to be read before writing such a page."
|
|
2038
|
+
"meant to be read before writing such a page. Topic `custom-domain`: the steps that put a page or site on a domain the user owns " +
|
|
2039
|
+
"(profile, DNS records, one check, statuses, problems and fixes).",
|
|
1937
2040
|
inputSchema: {
|
|
1938
|
-
topic: z.enum(["page-data"]).describe("Guide topic"),
|
|
2041
|
+
topic: z.enum(["page-data", "custom-domain"]).describe("Guide topic"),
|
|
1939
2042
|
},
|
|
1940
|
-
}, async () => textResult(PAGE_DATA_GUIDE + PAGE_DATA_GUIDE_TAIL));
|
|
2043
|
+
}, async ({ topic }) => textResult(topic === "custom-domain" ? CUSTOM_DOMAIN_GUIDE : PAGE_DATA_GUIDE + PAGE_DATA_GUIDE_TAIL));
|
|
1941
2044
|
return server;
|
|
1942
2045
|
}
|
package/package.json
CHANGED