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.
Files changed (3) hide show
  1. package/README.md +94 -34
  2. package/dist/index.js +110 -35
  3. 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 a stable HTTPS short URL -- 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 (keeping the same URL), delete the resource, or authenticate list operations across sessions. `brewpage-mcp` exposes this API as fourteen 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.
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
- Add to `~/.claude/settings.json`:
39
+ Register a project-scoped stdio server:
38
40
 
39
- ```json
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
- Fourteen tools are available, grouped by resource: **HTML**, **JSON**, **KV**, **Files**, **Sites**, and **Discovery**. All write operations return an owner token; keep it to modify or delete the resource later. Any update (`PUT`) operation requires the original `ownerToken` -- it is the backend's authorization gate. `password` is optional on reads/creates; if set, it is sent as the `X-Password` header.
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 to keep the page private/unlisted**: reachable only by its link, not in the gallery, not indexed by search engines; pass `public` only when the user explicitly wants it gallery-listed and search-indexed), `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).
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 `HTML` | `MARKDOWN` | `code`, defaults to `HTML`).
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 to list and index it), `filename` (optional custom filename).
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, mutually exclusive with `files`), `files` (array of `{path, content}`, mutually exclusive with `entryContent`), `entry` (optional entry file path, default `index.html`), `namespace` (optional -- omit to keep the site private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `password` (optional), `ttlDays` (1--30, default 15), `ownerToken` (optional, groups site under an existing owner).
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 or object, the JSON payload), `namespace` (optional -- omit to keep the document private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `password` (optional), `ttlDays` (optional, 1--30, default 15), `tags` (optional `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 or object, the new payload), `ownerToken` (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 to keep the KV store private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `password` (optional), `ttlDays` (optional, 1--30, default 15), `tags` (optional `string[]`).
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 API key under the label 'staging-token' so I can reuse it next session."
291
+ > "Store the selected theme so I can reuse it next session."
236
292
 
237
293
  ```
238
- publish_kv(key="staging-token", value="abc123")
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
- > "Update the staging-token value to the new secret."
307
+ > "Change the selected theme to light."
252
308
 
253
309
  ```
254
- set_kv(namespace="public", id="aBcDeFgHiJ", key="staging-token", value="def456", ownerToken="tok_...")
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 under staging-token?"
323
+ > "What theme did I store?"
268
324
 
269
325
  ```
270
- get_kv(namespace="public", id="aBcDeFgHiJ", key="staging-token")
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** -- pass `public` to gallery-list + index |
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 via `X-Owner-Token` on subsequent creates to group resources under one owner.
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 `get_page` (not needed for reads, but required for listing your own resources via the REST API).
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.7.0",
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
- "Publish with namespace `public` to list it in the gallery and have search engines index it.";
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 the page " +
37
- "listed in the brewpage.app gallery and indexed by search engines.";
38
- async function apiRequest(method, path, body, headers) {
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.url || data.link || "N/A"}`,
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 = (effectiveNamespace ?? data.namespace) || "";
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.url || data.link || "N/A"}`,
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 body = { content, format };
117
- body.namespace = effectiveNamespace;
118
- if (password)
119
- body.password = password;
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 { ok, data } = await apiRequest("POST", "/api/html", body);
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
- formData.append("namespace", effectiveNamespace);
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(data)}`,
311
+ text: `Failed to fetch page (${status}): ${JSON.stringify(errorData)}`,
252
312
  },
253
313
  ],
254
314
  isError: true,
255
315
  };
256
316
  }
257
- const page = data;
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: ${page.url || `${BASE_URL}/${namespace}/${id}`}`,
261
- `Format: ${page.format || "HTML"}`,
262
- `Created: ${page.createdAt || "N/A"}`,
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
- `${page.content || ""}`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brewpage-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "mcpName": "io.github.kochetkov-ma/brewpage-mcp",
5
5
  "description": "MCP server for BrewPage — publish and manage HTML, KV, JSON, and file content from AI assistants",
6
6
  "type": "module",