brewpage-mcp 1.7.0 → 1.9.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 +94 -34
- package/dist/index.js +110 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,14 +7,16 @@ MCP server for [BrewPage](https://brewpage.app) -- publish and manage HTML, KV,
|
|
|
7
7
|
|
|
8
8
|
## What is BrewPage
|
|
9
9
|
|
|
10
|
-
[BrewPage](https://brewpage.app) is a free instant hosting platform designed for AI agents and developers. One `POST` request publishes HTML, Markdown, a multi-file site, a JSON document, or a binary file and returns
|
|
10
|
+
[BrewPage](https://brewpage.app) is a free instant hosting platform designed for AI agents and developers. One `POST` request publishes HTML, Markdown, a multi-file site, a JSON document, or a binary file and returns its primary HTTPS link -- no accounts, no API keys, no infrastructure setup. Every resource carries an **owner token** returned at creation time: use it to update content in place, delete the resource, or authenticate list operations across sessions. `brewpage-mcp` exposes this API as sixteen typed MCP tools so any compatible agent -- Claude, Codex, Gemini, Cursor, Cline -- can publish, update, fetch, and manage BrewPage HTML, JSON, KV, and file content without leaving the conversation.
|
|
11
11
|
|
|
12
12
|
## Quick Start
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx brewpage-mcp
|
|
15
|
+
npx -y brewpage-mcp@1.9.0
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Requires Node.js 20 or newer. This starts the stdio server for an MCP client.
|
|
19
|
+
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
20
22
|
### Claude Desktop
|
|
@@ -26,7 +28,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
26
28
|
"mcpServers": {
|
|
27
29
|
"brewpage": {
|
|
28
30
|
"command": "npx",
|
|
29
|
-
"args": ["-y", "brewpage-mcp"]
|
|
31
|
+
"args": ["-y", "brewpage-mcp@1.9.0"]
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -34,19 +36,14 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
34
36
|
|
|
35
37
|
### Claude Code
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
Register a project-scoped stdio server:
|
|
38
40
|
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
"mcpServers": {
|
|
42
|
-
"brewpage": {
|
|
43
|
-
"command": "npx",
|
|
44
|
-
"args": ["-y", "brewpage-mcp"]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
41
|
+
```bash
|
|
42
|
+
claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.9.0
|
|
48
43
|
```
|
|
49
44
|
|
|
45
|
+
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).
|
|
46
|
+
|
|
50
47
|
### Cursor
|
|
51
48
|
|
|
52
49
|
Open **Settings > MCP** and add:
|
|
@@ -56,7 +53,7 @@ Open **Settings > MCP** and add:
|
|
|
56
53
|
"mcpServers": {
|
|
57
54
|
"brewpage": {
|
|
58
55
|
"command": "npx",
|
|
59
|
-
"args": ["-y", "brewpage-mcp"]
|
|
56
|
+
"args": ["-y", "brewpage-mcp@1.9.0"]
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
}
|
|
@@ -70,7 +67,7 @@ Open the Cline MCP settings panel and add:
|
|
|
70
67
|
{
|
|
71
68
|
"brewpage": {
|
|
72
69
|
"command": "npx",
|
|
73
|
-
"args": ["-y", "brewpage-mcp"]
|
|
70
|
+
"args": ["-y", "brewpage-mcp@1.9.0"]
|
|
74
71
|
}
|
|
75
72
|
}
|
|
76
73
|
```
|
|
@@ -78,13 +75,54 @@ Open the Cline MCP settings panel and add:
|
|
|
78
75
|
### Global Install
|
|
79
76
|
|
|
80
77
|
```bash
|
|
81
|
-
npm install -g brewpage-mcp
|
|
78
|
+
npm install -g brewpage-mcp@1.9.0
|
|
82
79
|
brewpage-mcp
|
|
83
80
|
```
|
|
84
81
|
|
|
85
82
|
## Tools
|
|
86
83
|
|
|
87
|
-
|
|
84
|
+
Sixteen tools are available. Creation returns an owner token; keep it to modify or delete the resource later. Updates (`PUT`/`PATCH`) require the original `ownerToken`. Supported password inputs use `X-Password`.
|
|
85
|
+
|
|
86
|
+
| Tool | Purpose |
|
|
87
|
+
|------|---------|
|
|
88
|
+
| `publish_html` | Publish HTML or Markdown |
|
|
89
|
+
| `update_html` | Replace page content; omitted format preserves stored format |
|
|
90
|
+
| `get_page` | Fetch raw page content |
|
|
91
|
+
| `publish_file` | Fetch a source URL and upload the file |
|
|
92
|
+
| `publish_site` | Publish a single page or complete multi-file site bundle |
|
|
93
|
+
| `republish_site` | Replace the complete site bundle at the same ID/link |
|
|
94
|
+
| `publish_json` | Publish a JSON object, array, or JSON-encoded string |
|
|
95
|
+
| `get_json` | Read a JSON document |
|
|
96
|
+
| `update_json` | Replace a JSON document |
|
|
97
|
+
| `publish_kv` | Create a KV store with an initial entry |
|
|
98
|
+
| `set_kv` | Set or replace a key in an owned KV store |
|
|
99
|
+
| `get_kv` | Read a KV value |
|
|
100
|
+
| `update_hosting` | Change eligible delivery mode with owner token and hosting version |
|
|
101
|
+
| `delete_resource` | Delete HTML, JSON, KV, or file content |
|
|
102
|
+
| `search_gallery` | Browse public content or list owned resources |
|
|
103
|
+
| `get_stats` | Read platform-wide statistics |
|
|
104
|
+
|
|
105
|
+
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 still generates an unlisted `priv-<random>` namespace; anyone with an unpassworded unlisted link can read it. `public` grants gallery/search eligibility only; password-protected content remains excluded.
|
|
106
|
+
|
|
107
|
+
For a fresh publication, sites and non-public namespaces require Dedicated subdomain (`subdomain`); explicit `path` is rejected. Other public publications default 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.
|
|
108
|
+
|
|
109
|
+
Responses use the server's primary `link` verbatim, including the root slash, and expose `routingCohort`, actual/requested/effective modes, `deliveryModeMatched`, `modeLocked`, `hostingVersion`, access and the trusted-apex `managementLink` when present. Never reconstruct a host from namespace or ID. NEW password links are clean URLs: do not add `?p=`; browser readers unlock through the trusted gateway. Owner tokens and API calls stay on the configured apex API origin.
|
|
110
|
+
|
|
111
|
+
When `deliveryMode` is omitted for a `public` HTML or Markdown page, the server may publish it on its own subdomain if the page needs browser features a BrewPage link does not provide. `publish_html` then prints `deliveryModeReason: "auto:<codes>"` and a `Delivery notice:` line with the server's explanation; pass `deliveryMode: "path"` to keep a BrewPage link, or change it later with `update_hosting`. `advisory:<codes>` (from `publish_html` or `update_html`) means the address was not changed but such features were found. Codes are comma-separated, drawn from `storage`, `cookies`, `history`, `downloads`, `top-navigation`, `media-capture`, `pointer-lock`, `embeds`, `fullscreen`. Neither line appears when nothing applies.
|
|
112
|
+
|
|
113
|
+
An existing deduplication winner keeps its actual URL, cohort and mode, even when another mode was requested. OLD publications display **Existing link**, with null mode metadata; their legacy links and behavior remain unchanged. Ordinary content updates and site republishing preserve hosting. An eligible NEW public non-site may change mode separately with `update_hosting`.
|
|
114
|
+
|
|
115
|
+
### Hosting
|
|
116
|
+
|
|
117
|
+
### `update_hosting`
|
|
118
|
+
|
|
119
|
+
Change an eligible NEW public non-site between Promotion and Dedicated subdomain. Parameters: `type` (`html` | `file` | `json` | `kv`), `namespace`, `id`, `ownerToken`, `deliveryMode` (`path` | `subdomain`), and `expectedVersion` (current server `hostingVersion`, integer ≥1).
|
|
120
|
+
|
|
121
|
+
The tool sends owner-only `PATCH /api/{html|files|json|kv}/{namespace}/{id}/hosting` with `{deliveryMode, expectedVersion}` and `X-Owner-Token`. OLD links, sites and non-public publications are locked. The server returns 403 for invalid owner authority, 400 for locked hosting, and 409 for a stale version; reload current metadata before retrying. A paused real change returns 503. Selecting the current mode returns 200 without incrementing the version. Use the returned primary link after a successful change; the publication host identity is retained through both directions.
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
update_hosting(type="html", namespace="public", id="aBcDeFgHiJ", ownerToken="tok_...", deliveryMode="subdomain", expectedVersion=1)
|
|
125
|
+
```
|
|
88
126
|
|
|
89
127
|
### HTML
|
|
90
128
|
|
|
@@ -92,7 +130,9 @@ Fourteen tools are available, grouped by resource: **HTML**, **JSON**, **KV**, *
|
|
|
92
130
|
|
|
93
131
|
Publish HTML or Markdown content to BrewPage. Returns a public URL and owner token.
|
|
94
132
|
|
|
95
|
-
Parameters: `content` (string), `format` (`HTML` | `MARKDOWN`, default `HTML`), `namespace` (optional -- **omit
|
|
133
|
+
Parameters: `content` (string), `format` (`HTML` | `MARKDOWN`, default `HTML`), `namespace` (optional -- **omit for unlisted content**; pass `public` only for gallery/search eligibility, except when password-protected), `password` (optional), `ttlDays` (1--30, default 15), `filename` (optional, used as title fallback), `showTopBar` (optional boolean -- adds a toolbar with filename, Download button, and theme toggle), `deliveryMode` (optional).
|
|
134
|
+
|
|
135
|
+
The result includes `deliveryModeReason` and a `Delivery notice:` line when the server returns them (see above).
|
|
96
136
|
|
|
97
137
|
Example prompt that invokes this tool:
|
|
98
138
|
|
|
@@ -108,7 +148,9 @@ publish_html(content="<h1>Report</h1>...", format="HTML", ttlDays=15)
|
|
|
108
148
|
|
|
109
149
|
Update an existing HTML or Markdown page in place, preserving its short URL. Requires the original `ownerToken` returned at creation.
|
|
110
150
|
|
|
111
|
-
Parameters: `namespace` (string), `id` (string), `content` (string, the new body), `ownerToken` (string), `format` (optional
|
|
151
|
+
Parameters: `namespace` (string), `id` (string), `content` (string, the new body), `ownerToken` (string), `format` (optional HTML/Markdown alias or supported code language such as `json`, `yaml`, or `typescript`; omitted preserves the stored format).
|
|
152
|
+
|
|
153
|
+
An update never changes the link or delivery mode; an advisory `deliveryModeReason` and `Delivery notice:` line are printed when the server returns them.
|
|
112
154
|
|
|
113
155
|
Example prompt:
|
|
114
156
|
|
|
@@ -124,6 +166,8 @@ update_html(namespace="public", id="aBcDeFgHiJ", content="<h1>Updated</h1>...",
|
|
|
124
166
|
|
|
125
167
|
Fetch the content of a published BrewPage HTML page by namespace and ID.
|
|
126
168
|
|
|
169
|
+
Returns the actual response body as text, including authored HTML or JSON code. Hosting metadata and the primary link come only from server response headers; JSON fields inside the publication are never treated as platform metadata. The existing apex HTML API route is retained.
|
|
170
|
+
|
|
127
171
|
Parameters: `namespace` (string), `id` (string), `password` (optional, if the page is password-protected).
|
|
128
172
|
|
|
129
173
|
Example prompt:
|
|
@@ -142,7 +186,7 @@ get_page(namespace="public", id="aBcDeFgHiJ")
|
|
|
142
186
|
|
|
143
187
|
Upload a file to BrewPage by fetching it from a URL. Returns a public URL and owner token. Supports images, PDFs, video, audio, code files, and archives.
|
|
144
188
|
|
|
145
|
-
Parameters: `url` (string, the source URL to fetch), `namespace` (optional -- omit to keep the file private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only
|
|
189
|
+
Parameters: `url` (string, the source URL to fetch), `namespace` (optional -- omit to keep the file private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only for gallery/search eligibility), `filename` (optional custom filename), `deliveryMode` (optional). This tool does not expose base64, password, or TTL inputs.
|
|
146
190
|
|
|
147
191
|
Example prompt:
|
|
148
192
|
|
|
@@ -160,7 +204,7 @@ publish_file(url="https://example.com/diagram.png")
|
|
|
160
204
|
|
|
161
205
|
Publish a single-page or multi-file HTML site. Pass `entryContent` for a single page or `files` (array of `{path, content}`) for a multi-file site. Supports password protection, TTL, and owner token grouping.
|
|
162
206
|
|
|
163
|
-
Parameters: `entryContent` (string
|
|
207
|
+
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 unlisted content; `public` grants gallery eligibility except when password-protected), `password` (optional), `ttlDays` (1--30, default 15), `ownerToken` (optional, groups site under an existing owner), `deliveryMode` (optional; fresh sites require `subdomain`).
|
|
164
208
|
|
|
165
209
|
Example prompt:
|
|
166
210
|
|
|
@@ -172,13 +216,25 @@ publish_site(files=[{"path": "index.html", "content": "..."}, {"path": "style.cs
|
|
|
172
216
|
|
|
173
217
|
---
|
|
174
218
|
|
|
219
|
+
### `republish_site`
|
|
220
|
+
|
|
221
|
+
Replace an owned site's complete file set at the same ID and primary link. Files absent from the new bundle are removed; matching files are overwritten and new files added. Hosting stays unchanged.
|
|
222
|
+
|
|
223
|
+
Parameters: `namespace`, `id`, `ownerToken`, exactly one of `entryContent` or a non-empty `files` array of `{path, content}`, optional `entry` (default `index.html`), `ttlDays` (1--30, default 15), and `tags`.
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
republish_site(namespace="public", id="aBcDeFgHiJ", ownerToken="tok_...", files=[{"path":"index.html","content":"<h1>Updated site</h1>"}])
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
175
231
|
### JSON
|
|
176
232
|
|
|
177
233
|
### `publish_json`
|
|
178
234
|
|
|
179
235
|
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.
|
|
180
236
|
|
|
181
|
-
Parameters: `json` (string
|
|
237
|
+
Parameters: `json` (string, object, or array), `namespace` (optional -- omit for unlisted content; `public` grants gallery/search eligibility, except when password-protected), `password` (optional), `ttlDays` (1--30, default 15), `tags` (optional `string[]`), `ownerToken` (optional existing owner), `deliveryMode` (optional).
|
|
182
238
|
|
|
183
239
|
Example prompt:
|
|
184
240
|
|
|
@@ -210,7 +266,7 @@ get_json(namespace="public", id="aBcDeFgHiJ")
|
|
|
210
266
|
|
|
211
267
|
Update an existing JSON document in place, preserving its short URL. Requires the original `ownerToken`.
|
|
212
268
|
|
|
213
|
-
Parameters: `namespace` (string), `id` (string), `json` (string
|
|
269
|
+
Parameters: `namespace` (string), `id` (string), `json` (string, object, or array), `ownerToken` (string).
|
|
214
270
|
|
|
215
271
|
Example prompt:
|
|
216
272
|
|
|
@@ -228,14 +284,14 @@ update_json(namespace="public", id="aBcDeFgHiJ", json={"mode": "dark", "version"
|
|
|
228
284
|
|
|
229
285
|
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.
|
|
230
286
|
|
|
231
|
-
Parameters: `key` (string), `value` (string), `namespace` (optional -- omit
|
|
287
|
+
Parameters: `key` (string), `value` (string), `namespace` (optional -- omit for unlisted content; `public` grants gallery/search eligibility, except when password-protected), `password` (optional), `ttlDays` (1--30, default 15), `tags` (optional `string[]`), `ownerToken` (optional existing owner), `deliveryMode` (optional).
|
|
232
288
|
|
|
233
289
|
Example prompt:
|
|
234
290
|
|
|
235
|
-
> "Store the
|
|
291
|
+
> "Store the selected theme so I can reuse it next session."
|
|
236
292
|
|
|
237
293
|
```
|
|
238
|
-
publish_kv(key="
|
|
294
|
+
publish_kv(key="theme", value="dark")
|
|
239
295
|
```
|
|
240
296
|
|
|
241
297
|
---
|
|
@@ -248,10 +304,10 @@ Parameters: `namespace` (string), `id` (string), `key` (string), `value` (string
|
|
|
248
304
|
|
|
249
305
|
Example prompt:
|
|
250
306
|
|
|
251
|
-
> "
|
|
307
|
+
> "Change the selected theme to light."
|
|
252
308
|
|
|
253
309
|
```
|
|
254
|
-
set_kv(namespace="public", id="aBcDeFgHiJ", key="
|
|
310
|
+
set_kv(namespace="public", id="aBcDeFgHiJ", key="theme", value="light", ownerToken="tok_...")
|
|
255
311
|
```
|
|
256
312
|
|
|
257
313
|
---
|
|
@@ -264,10 +320,10 @@ Parameters: `namespace` (string), `id` (string), `key` (string), `password` (opt
|
|
|
264
320
|
|
|
265
321
|
Example prompt:
|
|
266
322
|
|
|
267
|
-
> "What did I store
|
|
323
|
+
> "What theme did I store?"
|
|
268
324
|
|
|
269
325
|
```
|
|
270
|
-
get_kv(namespace="public", id="aBcDeFgHiJ", key="
|
|
326
|
+
get_kv(namespace="public", id="aBcDeFgHiJ", key="theme")
|
|
271
327
|
```
|
|
272
328
|
|
|
273
329
|
---
|
|
@@ -298,6 +354,8 @@ Delete a BrewPage resource (HTML page, KV store, JSON collection, or file) using
|
|
|
298
354
|
|
|
299
355
|
Parameters: `type` (`html` | `kv` | `json` | `file`), `namespace` (string), `id` (string), `ownerToken` (string).
|
|
300
356
|
|
|
357
|
+
Sites are not supported by this tool; use the REST site deletion endpoint.
|
|
358
|
+
|
|
301
359
|
Example prompt:
|
|
302
360
|
|
|
303
361
|
> "Delete the page I just published -- namespace public, id aBcDeFgHiJ."
|
|
@@ -328,9 +386,11 @@ get_stats()
|
|
|
328
386
|
|--------------------|------|-------|
|
|
329
387
|
| "Publish this HTML so I can share it" | `publish_html` | `format=HTML`; save `ownerToken` |
|
|
330
388
|
| "Share these meeting notes as a link" | `publish_html` | `format=MARKDOWN`; readable rendered output |
|
|
331
|
-
| "Host this AI-generated artifact" | `publish_html` | `namespace` optional; **private/unlisted by default
|
|
389
|
+
| "Host this AI-generated artifact" | `publish_html` | `namespace` optional; **private/unlisted by default**; `public` grants gallery/search eligibility except when protected |
|
|
332
390
|
| "Upload this image / PDF / video" | `publish_file` | Fetches from URL; inline preview on short URL |
|
|
333
391
|
| "Deploy this static site" | `publish_site` | Pass `files` array or `entryContent` |
|
|
392
|
+
| "Replace the site I published" | `republish_site` | Owner token; complete bundle replaces all existing files |
|
|
393
|
+
| "Give my public page a dedicated address" | `update_hosting` | NEW public non-site; owner token + current hosting version |
|
|
334
394
|
| "Fix a typo in the page I shared -- same link" | `update_html` | Requires `ownerToken`; URL stays the same |
|
|
335
395
|
| "Remove the page I published" | `delete_resource` | Requires `ownerToken` from creation |
|
|
336
396
|
| "Read back the page I published" | `get_page` | Returns raw content for editing |
|
|
@@ -348,15 +408,15 @@ get_stats()
|
|
|
348
408
|
|
|
349
409
|
Every publish response includes an **owner token** -- the only credential that allows updating or deleting your content. **Save it. It cannot be recovered.**
|
|
350
410
|
|
|
351
|
-
- Reuse it
|
|
411
|
+
- Reuse it with `publish_site`, `publish_json`, or `publish_kv` to group new resources under one owner; REST creates accept `X-Owner-Token`.
|
|
352
412
|
- Pass it to `delete_resource` to remove content.
|
|
353
|
-
- Pass it to `
|
|
413
|
+
- Pass it to `search_gallery` with `mine=true` to list owned resources. `get_page` has no owner-token input; protected reads use `password`.
|
|
354
414
|
|
|
355
415
|
## Configuration
|
|
356
416
|
|
|
357
417
|
| Variable | Default | Description |
|
|
358
418
|
|----------|---------|-------------|
|
|
359
|
-
| `BREWPAGE_URL` | `https://brewpage.app` | API base URL |
|
|
419
|
+
| `BREWPAGE_URL` | `https://brewpage.app` | Authorized apex API base URL; never a publication host |
|
|
360
420
|
|
|
361
421
|
## Links
|
|
362
422
|
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
const BASE_URL = process.env.BREWPAGE_URL || "https://brewpage.app";
|
|
6
6
|
const server = new McpServer({
|
|
7
7
|
name: "brewpage-mcp",
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.9.0",
|
|
9
9
|
});
|
|
10
10
|
const PUBLIC_NAMESPACE = "public";
|
|
11
11
|
/**
|
|
@@ -30,12 +30,37 @@ function resolveNamespace(namespace) {
|
|
|
30
30
|
return namespace && namespace.length > 0 ? namespace : defaultPrivateNamespace();
|
|
31
31
|
}
|
|
32
32
|
const UNLISTED_NOTICE = "Unlisted link — anyone who has it can open it, but it's not in the gallery or search. " +
|
|
33
|
-
"
|
|
33
|
+
"Use namespace `public` for gallery/search eligibility; password-protected content stays excluded.";
|
|
34
|
+
const PROTECTED_UNLISTED_NOTICE = "Password-protected unlisted link — readers need the password; excluded from gallery and search.";
|
|
34
35
|
const NAMESPACE_DESCRIPTION = "Optional. Omit to keep the content PRIVATE/unlisted (reachable only by its link — " +
|
|
35
36
|
"not listed in the gallery and not indexed by search engines). Pass a custom namespace " +
|
|
36
|
-
"to group private content, or pass `public` ONLY when the user explicitly wants
|
|
37
|
-
"
|
|
38
|
-
|
|
37
|
+
"to group private content, or pass `public` ONLY when the user explicitly wants gallery/search " +
|
|
38
|
+
"eligibility. Password-protected content stays excluded from gallery/search.";
|
|
39
|
+
const DELIVERY_MODE_SCHEMA = z.enum(["path", "subdomain"]).optional().describe("Optional delivery choice for a fresh publication, independent of password/namespace. " +
|
|
40
|
+
"Public non-sites default to path (Promotion); fresh sites and non-public content require subdomain. " +
|
|
41
|
+
"An existing dedup winner keeps its actual mode and URL.");
|
|
42
|
+
function hostingResponseLines(data) {
|
|
43
|
+
const fields = ["routingCohort", "deliveryMode", "requestedDeliveryMode", "effectiveDeliveryMode",
|
|
44
|
+
"deliveryModeMatched", "modeLocked", "hostingVersion", "access", "managementLink"];
|
|
45
|
+
const lines = fields.filter((field) => field in data).map((field) => `${field}: ${JSON.stringify(data[field])}`);
|
|
46
|
+
if (data.routingCohort === "old")
|
|
47
|
+
lines.push("Hosting: Existing link");
|
|
48
|
+
if (data.deliveryModeMatched === false)
|
|
49
|
+
lines.push("Requested hosting differs from the existing winner; its URL and mode were preserved.");
|
|
50
|
+
lines.push(...deliveryReasonLines(data));
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
/** Optional HTML create/update fields: why the delivery mode was chosen or flagged. */
|
|
54
|
+
function deliveryReasonLines(data) {
|
|
55
|
+
const { deliveryModeReason: reason, deliveryNotice: notice } = data;
|
|
56
|
+
const lines = [];
|
|
57
|
+
if (typeof reason === "string" && reason.length > 0)
|
|
58
|
+
lines.push(`deliveryModeReason: ${JSON.stringify(reason)}`);
|
|
59
|
+
if (typeof notice === "string" && notice.length > 0)
|
|
60
|
+
lines.push(`Delivery notice: ${notice}`);
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
async function apiRequest(method, path, body, headers, responseType = "json") {
|
|
39
64
|
const url = `${BASE_URL}${path}`;
|
|
40
65
|
const res = await fetch(url, {
|
|
41
66
|
method,
|
|
@@ -45,24 +70,25 @@ async function apiRequest(method, path, body, headers) {
|
|
|
45
70
|
},
|
|
46
71
|
body: body ? JSON.stringify(body) : undefined,
|
|
47
72
|
});
|
|
48
|
-
const data = await res.json().catch(() => null);
|
|
49
|
-
return { ok: res.ok, status: res.status, data };
|
|
73
|
+
const data = responseType === "text" ? await res.text() : await res.json().catch(() => null);
|
|
74
|
+
return { ok: res.ok, status: res.status, data, headers: res.headers };
|
|
50
75
|
}
|
|
51
76
|
function formatPublishResponse(data, effectiveNamespace) {
|
|
52
77
|
const lines = [
|
|
53
78
|
`Published successfully!`,
|
|
54
79
|
``,
|
|
55
|
-
`URL: ${data.
|
|
80
|
+
`URL: ${data.link || data.url || "N/A"}`,
|
|
56
81
|
];
|
|
57
82
|
if (data.shortUrl)
|
|
58
83
|
lines.push(`Short URL: ${data.shortUrl}`);
|
|
59
84
|
lines.push(`Namespace: ${data.namespace || "N/A"}`, `ID: ${data.id || "N/A"}`);
|
|
60
85
|
if (data.expiresAt)
|
|
61
86
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
87
|
+
lines.push(...hostingResponseLines(data));
|
|
62
88
|
lines.push(``, `===================================`, `OWNER TOKEN: ${data.ownerToken}`, `===================================`, ``, `IMPORTANT: Save your owner token! You need it to update or delete this resource later. It cannot be recovered if lost.`);
|
|
63
|
-
const ns = (
|
|
89
|
+
const ns = (data.namespace ?? effectiveNamespace) || "";
|
|
64
90
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
65
|
-
lines.push(``, UNLISTED_NOTICE);
|
|
91
|
+
lines.push(``, data.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
66
92
|
}
|
|
67
93
|
return lines.join("\n");
|
|
68
94
|
}
|
|
@@ -70,7 +96,7 @@ function formatUpdateResponse(data) {
|
|
|
70
96
|
const lines = [
|
|
71
97
|
`Updated successfully!`,
|
|
72
98
|
``,
|
|
73
|
-
`URL: ${data.
|
|
99
|
+
`URL: ${data.link || data.url || "N/A"}`,
|
|
74
100
|
];
|
|
75
101
|
if (data.shortUrl)
|
|
76
102
|
lines.push(`Short URL: ${data.shortUrl}`);
|
|
@@ -79,10 +105,14 @@ function formatUpdateResponse(data) {
|
|
|
79
105
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
80
106
|
if (data.updatedAt)
|
|
81
107
|
lines.push(`Updated: ${data.updatedAt}`);
|
|
108
|
+
lines.push(...hostingResponseLines(data));
|
|
82
109
|
return lines.join("\n");
|
|
83
110
|
}
|
|
84
|
-
server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Returns a public URL and owner token. Supports password protection, custom TTL, optional filename, and an opt-in top toolbar (showTopBar)."
|
|
111
|
+
server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Returns a public URL and owner token. Supports password protection, custom TTL, optional filename, and an opt-in top toolbar (showTopBar). " +
|
|
112
|
+
"When deliveryMode is omitted for a `public` page, the server may publish it on its own subdomain if the page needs browser features a BrewPage link does not provide; " +
|
|
113
|
+
"the result then includes deliveryModeReason and a delivery notice. Always share the returned URL.", {
|
|
85
114
|
content: z.string().describe("HTML or Markdown content to publish"),
|
|
115
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
86
116
|
format: z
|
|
87
117
|
.enum(["HTML", "MARKDOWN"])
|
|
88
118
|
.default("HTML")
|
|
@@ -111,19 +141,22 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
|
|
|
111
141
|
.boolean()
|
|
112
142
|
.optional()
|
|
113
143
|
.describe("Add a thin top toolbar (filename + Download button + theme toggle) on the served page. Default: hidden."),
|
|
114
|
-
}, async ({ content, format, namespace, password, ttlDays, filename, showTopBar }) => {
|
|
144
|
+
}, async ({ content, format, namespace, password, ttlDays, filename, showTopBar, deliveryMode }) => {
|
|
115
145
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (ttlDays)
|
|
121
|
-
body.ttlDays = ttlDays;
|
|
146
|
+
const qsParams = [`ns=${encodeURIComponent(effectiveNamespace)}`, `format=${encodeURIComponent(format.toLowerCase())}`];
|
|
147
|
+
if (ttlDays !== undefined)
|
|
148
|
+
qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
|
|
149
|
+
const body = { content };
|
|
122
150
|
if (filename)
|
|
123
151
|
body.filename = filename;
|
|
124
152
|
if (showTopBar !== undefined)
|
|
125
153
|
body.showTopBar = showTopBar;
|
|
126
|
-
const
|
|
154
|
+
const headers = {};
|
|
155
|
+
if (password)
|
|
156
|
+
headers["X-Password"] = password;
|
|
157
|
+
if (deliveryMode)
|
|
158
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
159
|
+
const { ok, data } = await apiRequest("POST", `/api/html?${qsParams.join("&")}`, body, headers);
|
|
127
160
|
if (!ok) {
|
|
128
161
|
return {
|
|
129
162
|
content: [
|
|
@@ -146,12 +179,13 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
|
|
|
146
179
|
});
|
|
147
180
|
server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public URL and owner token.", {
|
|
148
181
|
url: z.string().url().describe("URL of the file to upload"),
|
|
182
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
149
183
|
namespace: z
|
|
150
184
|
.string()
|
|
151
185
|
.optional()
|
|
152
186
|
.describe(NAMESPACE_DESCRIPTION),
|
|
153
187
|
filename: z.string().optional().describe("Custom filename (optional)"),
|
|
154
|
-
}, async ({ url: fileUrl, namespace, filename }) => {
|
|
188
|
+
}, async ({ url: fileUrl, namespace, filename, deliveryMode }) => {
|
|
155
189
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
156
190
|
const fileRes = await fetch(fileUrl);
|
|
157
191
|
if (!fileRes.ok) {
|
|
@@ -168,9 +202,9 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
|
|
|
168
202
|
const blob = await fileRes.blob();
|
|
169
203
|
const formData = new FormData();
|
|
170
204
|
formData.append("file", blob, filename || fileUrl.split("/").pop() || "file");
|
|
171
|
-
|
|
172
|
-
const res = await fetch(`${BASE_URL}/api/files`, {
|
|
205
|
+
const res = await fetch(`${BASE_URL}/api/files?ns=${encodeURIComponent(effectiveNamespace)}`, {
|
|
173
206
|
method: "POST",
|
|
207
|
+
headers: deliveryMode ? { "X-Delivery-Mode": deliveryMode } : undefined,
|
|
174
208
|
body: formData,
|
|
175
209
|
});
|
|
176
210
|
const data = await res.json().catch(() => null);
|
|
@@ -194,6 +228,27 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
|
|
|
194
228
|
],
|
|
195
229
|
};
|
|
196
230
|
});
|
|
231
|
+
server.tool("update_hosting", "Change delivery mode for a NEW public non-site publication. Requires owner token and current hostingVersion. OLD links and forced subdomain modes are locked; reload after a 409 conflict. Content updates never change hosting.", {
|
|
232
|
+
type: z.enum(["html", "file", "json", "kv"]).describe("Resource type; sites have no hosting-choice endpoint"),
|
|
233
|
+
namespace: z.string().describe("Resource namespace; only public is eligible"),
|
|
234
|
+
id: z.string().describe("Resource ID"),
|
|
235
|
+
ownerToken: z.string().describe("Original resource owner token"),
|
|
236
|
+
deliveryMode: z.enum(["path", "subdomain"]).describe("path = Promotion; subdomain = Dedicated subdomain"),
|
|
237
|
+
expectedVersion: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).describe("Current hostingVersion from the server; required for concurrency control"),
|
|
238
|
+
}, async ({ type, namespace, id, ownerToken, deliveryMode, expectedVersion }) => {
|
|
239
|
+
const segment = type === "file" ? "files" : type;
|
|
240
|
+
const path = `/api/${segment}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/hosting`;
|
|
241
|
+
const { ok, status, data } = await apiRequest("PATCH", path, { deliveryMode, expectedVersion }, { "X-Owner-Token": ownerToken });
|
|
242
|
+
if (!ok) {
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: `Failed to update hosting (${status}): ${JSON.stringify(data)}` }],
|
|
245
|
+
isError: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: formatUpdateResponse(data) }],
|
|
250
|
+
};
|
|
251
|
+
});
|
|
197
252
|
server.tool("delete_resource", "Delete a BrewPage resource (HTML page, KV store, JSON collection, or file) using the owner token.", {
|
|
198
253
|
type: z
|
|
199
254
|
.enum(["html", "kv", "json", "file"])
|
|
@@ -242,27 +297,35 @@ server.tool("get_page", "Fetch a published BrewPage HTML page content by namespa
|
|
|
242
297
|
const headers = {};
|
|
243
298
|
if (password)
|
|
244
299
|
headers["X-Password"] = password;
|
|
245
|
-
const { ok, status, data } = await apiRequest("GET", `/api/html/${namespace}/${id}`, undefined, headers);
|
|
300
|
+
const { ok, status, data, headers: responseHeaders } = await apiRequest("GET", `/api/html/${namespace}/${id}`, undefined, headers, "text");
|
|
246
301
|
if (!ok) {
|
|
302
|
+
let errorData = data;
|
|
303
|
+
try {
|
|
304
|
+
errorData = JSON.parse(data);
|
|
305
|
+
}
|
|
306
|
+
catch { }
|
|
247
307
|
return {
|
|
248
308
|
content: [
|
|
249
309
|
{
|
|
250
310
|
type: "text",
|
|
251
|
-
text: `Failed to fetch page (${status}): ${JSON.stringify(
|
|
311
|
+
text: `Failed to fetch page (${status}): ${JSON.stringify(errorData)}`,
|
|
252
312
|
},
|
|
253
313
|
],
|
|
254
314
|
isError: true,
|
|
255
315
|
};
|
|
256
316
|
}
|
|
257
|
-
const
|
|
317
|
+
const routingCohort = responseHeaders.get("X-Routing-Cohort");
|
|
318
|
+
const deliveryMode = responseHeaders.get("X-Delivery-Mode");
|
|
319
|
+
const hosting = { ...(routingCohort !== null ? { routingCohort } : {}), ...(deliveryMode !== null ? { deliveryMode } : {}) };
|
|
320
|
+
const hasNewMarkers = (routingCohort !== null && routingCohort !== "old") || deliveryMode !== null;
|
|
258
321
|
const lines = [
|
|
259
322
|
`Page: ${namespace}/${id}`,
|
|
260
|
-
`URL: ${
|
|
261
|
-
`
|
|
262
|
-
|
|
323
|
+
`URL: ${responseHeaders.get("X-Canonical-Link") ?? (hasNewMarkers ? "N/A" : `${BASE_URL}/${namespace}/${id}`)}`,
|
|
324
|
+
`Content-Type: ${responseHeaders.get("Content-Type") || "N/A"}`,
|
|
325
|
+
...hostingResponseLines(hosting),
|
|
263
326
|
``,
|
|
264
327
|
`--- Content ---`,
|
|
265
|
-
|
|
328
|
+
data,
|
|
266
329
|
];
|
|
267
330
|
return {
|
|
268
331
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -297,6 +360,7 @@ server.tool("get_stats", "Get BrewPage platform-wide usage statistics including
|
|
|
297
360
|
};
|
|
298
361
|
});
|
|
299
362
|
server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass `entryContent`. Multi-page: pass `files` (array of `{path, content}`). Optional `entry` overrides default `index.html`.", {
|
|
363
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
300
364
|
entryContent: z
|
|
301
365
|
.string()
|
|
302
366
|
.optional()
|
|
@@ -327,7 +391,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
327
391
|
.string()
|
|
328
392
|
.optional()
|
|
329
393
|
.describe("Existing owner token to group this site under the same owner as previous content"),
|
|
330
|
-
}, async ({ entryContent, files, entry, namespace, password, ttlDays, ownerToken }) => {
|
|
394
|
+
}, async ({ entryContent, files, entry, namespace, password, ttlDays, ownerToken, deliveryMode }) => {
|
|
331
395
|
const hasEntry = typeof entryContent === "string";
|
|
332
396
|
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
333
397
|
if (hasEntry === hasFiles) {
|
|
@@ -359,6 +423,8 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
359
423
|
headers["X-Password"] = password;
|
|
360
424
|
if (ownerToken)
|
|
361
425
|
headers["X-Owner-Token"] = ownerToken;
|
|
426
|
+
if (deliveryMode)
|
|
427
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
362
428
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
363
429
|
const qsParams = [];
|
|
364
430
|
if (entry)
|
|
@@ -399,10 +465,11 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
399
465
|
lines.push(`Tags: ${site.tags.join(", ")}`);
|
|
400
466
|
if (site.expiresAt)
|
|
401
467
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
468
|
+
lines.push(...hostingResponseLines(site));
|
|
402
469
|
lines.push(``, `===================================`, `OWNER TOKEN: ${site.ownerToken}`, `===================================`, ``, `IMPORTANT: Save your owner token! You need it to view info or delete this site. It cannot be recovered if lost.`);
|
|
403
470
|
const ns = (site.namespace || effectiveNamespace) || "";
|
|
404
471
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
405
|
-
lines.push(``, UNLISTED_NOTICE);
|
|
472
|
+
lines.push(``, site.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
406
473
|
}
|
|
407
474
|
return {
|
|
408
475
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -506,13 +573,15 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
506
573
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
507
574
|
if (site.result)
|
|
508
575
|
lines.push(`Result: ${site.result}`);
|
|
576
|
+
lines.push(...hostingResponseLines(site));
|
|
509
577
|
if (site.visibilityNotice)
|
|
510
578
|
lines.push(``, String(site.visibilityNotice));
|
|
511
579
|
return {
|
|
512
580
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
513
581
|
};
|
|
514
582
|
});
|
|
515
|
-
server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requires owner token. Returns the updated page metadata."
|
|
583
|
+
server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requires owner token. Returns the updated page metadata. " +
|
|
584
|
+
"The link and delivery mode never change on update; an advisory deliveryModeReason and delivery notice are included when the new content uses browser features that may not work fully on a BrewPage link.", {
|
|
516
585
|
namespace: z.string().describe("Page namespace"),
|
|
517
586
|
id: z.string().describe("Page ID"),
|
|
518
587
|
content: z.string().describe("New HTML or Markdown content"),
|
|
@@ -552,6 +621,7 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
552
621
|
};
|
|
553
622
|
});
|
|
554
623
|
server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON object, array, or a JSON-encoded string. Returns a public URL and owner token.", {
|
|
624
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
555
625
|
json: z
|
|
556
626
|
.union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
|
|
557
627
|
.describe("JSON document to publish (object, array, or JSON-encoded string)"),
|
|
@@ -578,7 +648,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
|
|
|
578
648
|
.string()
|
|
579
649
|
.optional()
|
|
580
650
|
.describe("Existing owner token to group this document under the same owner as previous content"),
|
|
581
|
-
}, async ({ json, namespace, password, ttlDays, tags, ownerToken }) => {
|
|
651
|
+
}, async ({ json, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
582
652
|
const jsonString = typeof json === "string" ? json : JSON.stringify(json);
|
|
583
653
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
584
654
|
const qsParams = [];
|
|
@@ -595,6 +665,8 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
|
|
|
595
665
|
headers["X-Password"] = password;
|
|
596
666
|
if (ownerToken)
|
|
597
667
|
headers["X-Owner-Token"] = ownerToken;
|
|
668
|
+
if (deliveryMode)
|
|
669
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
598
670
|
const res = await fetch(`${BASE_URL}/api/json${qs}`, {
|
|
599
671
|
method: "POST",
|
|
600
672
|
headers,
|
|
@@ -696,6 +768,7 @@ server.tool("update_json", "Update an existing BrewPage JSON document. Requires
|
|
|
696
768
|
};
|
|
697
769
|
});
|
|
698
770
|
server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV store; the entry is addressed by (namespace, id, key). Returns a public URL and owner token.", {
|
|
771
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
699
772
|
key: z.string().describe("KV key (first part of the (namespace, id, key) addressing tuple)"),
|
|
700
773
|
value: z.string().describe("KV value"),
|
|
701
774
|
namespace: z
|
|
@@ -721,7 +794,7 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
721
794
|
.string()
|
|
722
795
|
.optional()
|
|
723
796
|
.describe("Existing owner token to group this KV store under the same owner as previous content"),
|
|
724
|
-
}, async ({ key, value, namespace, password, ttlDays, tags, ownerToken }) => {
|
|
797
|
+
}, async ({ key, value, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
725
798
|
const body = { key, value };
|
|
726
799
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
727
800
|
const qsParams = [];
|
|
@@ -738,6 +811,8 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
738
811
|
headers["X-Password"] = password;
|
|
739
812
|
if (ownerToken)
|
|
740
813
|
headers["X-Owner-Token"] = ownerToken;
|
|
814
|
+
if (deliveryMode)
|
|
815
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
741
816
|
const { ok, status, data } = await apiRequest("POST", `/api/kv${qs}`, body, headers);
|
|
742
817
|
if (!ok) {
|
|
743
818
|
return {
|
package/package.json
CHANGED