brewpage-mcp 1.7.0 → 1.8.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 +88 -34
- package/dist/index.js +94 -33
- 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.8.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.8.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.8.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.8.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.8.0"]
|
|
74
71
|
}
|
|
75
72
|
}
|
|
76
73
|
```
|
|
@@ -78,13 +75,52 @@ 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.8.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
|
+
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`.
|
|
112
|
+
|
|
113
|
+
### Hosting
|
|
114
|
+
|
|
115
|
+
### `update_hosting`
|
|
116
|
+
|
|
117
|
+
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).
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
update_hosting(type="html", namespace="public", id="aBcDeFgHiJ", ownerToken="tok_...", deliveryMode="subdomain", expectedVersion=1)
|
|
123
|
+
```
|
|
88
124
|
|
|
89
125
|
### HTML
|
|
90
126
|
|
|
@@ -92,7 +128,7 @@ Fourteen tools are available, grouped by resource: **HTML**, **JSON**, **KV**, *
|
|
|
92
128
|
|
|
93
129
|
Publish HTML or Markdown content to BrewPage. Returns a public URL and owner token.
|
|
94
130
|
|
|
95
|
-
Parameters: `content` (string), `format` (`HTML` | `MARKDOWN`, default `HTML`), `namespace` (optional -- **omit
|
|
131
|
+
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).
|
|
96
132
|
|
|
97
133
|
Example prompt that invokes this tool:
|
|
98
134
|
|
|
@@ -108,7 +144,7 @@ publish_html(content="<h1>Report</h1>...", format="HTML", ttlDays=15)
|
|
|
108
144
|
|
|
109
145
|
Update an existing HTML or Markdown page in place, preserving its short URL. Requires the original `ownerToken` returned at creation.
|
|
110
146
|
|
|
111
|
-
Parameters: `namespace` (string), `id` (string), `content` (string, the new body), `ownerToken` (string), `format` (optional
|
|
147
|
+
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).
|
|
112
148
|
|
|
113
149
|
Example prompt:
|
|
114
150
|
|
|
@@ -124,6 +160,8 @@ update_html(namespace="public", id="aBcDeFgHiJ", content="<h1>Updated</h1>...",
|
|
|
124
160
|
|
|
125
161
|
Fetch the content of a published BrewPage HTML page by namespace and ID.
|
|
126
162
|
|
|
163
|
+
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.
|
|
164
|
+
|
|
127
165
|
Parameters: `namespace` (string), `id` (string), `password` (optional, if the page is password-protected).
|
|
128
166
|
|
|
129
167
|
Example prompt:
|
|
@@ -142,7 +180,7 @@ get_page(namespace="public", id="aBcDeFgHiJ")
|
|
|
142
180
|
|
|
143
181
|
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
182
|
|
|
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
|
|
183
|
+
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
184
|
|
|
147
185
|
Example prompt:
|
|
148
186
|
|
|
@@ -160,7 +198,7 @@ publish_file(url="https://example.com/diagram.png")
|
|
|
160
198
|
|
|
161
199
|
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
200
|
|
|
163
|
-
Parameters: `entryContent` (string
|
|
201
|
+
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
202
|
|
|
165
203
|
Example prompt:
|
|
166
204
|
|
|
@@ -172,13 +210,25 @@ publish_site(files=[{"path": "index.html", "content": "..."}, {"path": "style.cs
|
|
|
172
210
|
|
|
173
211
|
---
|
|
174
212
|
|
|
213
|
+
### `republish_site`
|
|
214
|
+
|
|
215
|
+
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.
|
|
216
|
+
|
|
217
|
+
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`.
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
republish_site(namespace="public", id="aBcDeFgHiJ", ownerToken="tok_...", files=[{"path":"index.html","content":"<h1>Updated site</h1>"}])
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
175
225
|
### JSON
|
|
176
226
|
|
|
177
227
|
### `publish_json`
|
|
178
228
|
|
|
179
229
|
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
230
|
|
|
181
|
-
Parameters: `json` (string
|
|
231
|
+
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
232
|
|
|
183
233
|
Example prompt:
|
|
184
234
|
|
|
@@ -210,7 +260,7 @@ get_json(namespace="public", id="aBcDeFgHiJ")
|
|
|
210
260
|
|
|
211
261
|
Update an existing JSON document in place, preserving its short URL. Requires the original `ownerToken`.
|
|
212
262
|
|
|
213
|
-
Parameters: `namespace` (string), `id` (string), `json` (string
|
|
263
|
+
Parameters: `namespace` (string), `id` (string), `json` (string, object, or array), `ownerToken` (string).
|
|
214
264
|
|
|
215
265
|
Example prompt:
|
|
216
266
|
|
|
@@ -228,14 +278,14 @@ update_json(namespace="public", id="aBcDeFgHiJ", json={"mode": "dark", "version"
|
|
|
228
278
|
|
|
229
279
|
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
280
|
|
|
231
|
-
Parameters: `key` (string), `value` (string), `namespace` (optional -- omit
|
|
281
|
+
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
282
|
|
|
233
283
|
Example prompt:
|
|
234
284
|
|
|
235
|
-
> "Store the
|
|
285
|
+
> "Store the selected theme so I can reuse it next session."
|
|
236
286
|
|
|
237
287
|
```
|
|
238
|
-
publish_kv(key="
|
|
288
|
+
publish_kv(key="theme", value="dark")
|
|
239
289
|
```
|
|
240
290
|
|
|
241
291
|
---
|
|
@@ -248,10 +298,10 @@ Parameters: `namespace` (string), `id` (string), `key` (string), `value` (string
|
|
|
248
298
|
|
|
249
299
|
Example prompt:
|
|
250
300
|
|
|
251
|
-
> "
|
|
301
|
+
> "Change the selected theme to light."
|
|
252
302
|
|
|
253
303
|
```
|
|
254
|
-
set_kv(namespace="public", id="aBcDeFgHiJ", key="
|
|
304
|
+
set_kv(namespace="public", id="aBcDeFgHiJ", key="theme", value="light", ownerToken="tok_...")
|
|
255
305
|
```
|
|
256
306
|
|
|
257
307
|
---
|
|
@@ -264,10 +314,10 @@ Parameters: `namespace` (string), `id` (string), `key` (string), `password` (opt
|
|
|
264
314
|
|
|
265
315
|
Example prompt:
|
|
266
316
|
|
|
267
|
-
> "What did I store
|
|
317
|
+
> "What theme did I store?"
|
|
268
318
|
|
|
269
319
|
```
|
|
270
|
-
get_kv(namespace="public", id="aBcDeFgHiJ", key="
|
|
320
|
+
get_kv(namespace="public", id="aBcDeFgHiJ", key="theme")
|
|
271
321
|
```
|
|
272
322
|
|
|
273
323
|
---
|
|
@@ -298,6 +348,8 @@ Delete a BrewPage resource (HTML page, KV store, JSON collection, or file) using
|
|
|
298
348
|
|
|
299
349
|
Parameters: `type` (`html` | `kv` | `json` | `file`), `namespace` (string), `id` (string), `ownerToken` (string).
|
|
300
350
|
|
|
351
|
+
Sites are not supported by this tool; use the REST site deletion endpoint.
|
|
352
|
+
|
|
301
353
|
Example prompt:
|
|
302
354
|
|
|
303
355
|
> "Delete the page I just published -- namespace public, id aBcDeFgHiJ."
|
|
@@ -328,9 +380,11 @@ get_stats()
|
|
|
328
380
|
|--------------------|------|-------|
|
|
329
381
|
| "Publish this HTML so I can share it" | `publish_html` | `format=HTML`; save `ownerToken` |
|
|
330
382
|
| "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
|
|
383
|
+
| "Host this AI-generated artifact" | `publish_html` | `namespace` optional; **private/unlisted by default**; `public` grants gallery/search eligibility except when protected |
|
|
332
384
|
| "Upload this image / PDF / video" | `publish_file` | Fetches from URL; inline preview on short URL |
|
|
333
385
|
| "Deploy this static site" | `publish_site` | Pass `files` array or `entryContent` |
|
|
386
|
+
| "Replace the site I published" | `republish_site` | Owner token; complete bundle replaces all existing files |
|
|
387
|
+
| "Give my public page a dedicated address" | `update_hosting` | NEW public non-site; owner token + current hosting version |
|
|
334
388
|
| "Fix a typo in the page I shared -- same link" | `update_html` | Requires `ownerToken`; URL stays the same |
|
|
335
389
|
| "Remove the page I published" | `delete_resource` | Requires `ownerToken` from creation |
|
|
336
390
|
| "Read back the page I published" | `get_page` | Returns raw content for editing |
|
|
@@ -348,15 +402,15 @@ get_stats()
|
|
|
348
402
|
|
|
349
403
|
Every publish response includes an **owner token** -- the only credential that allows updating or deleting your content. **Save it. It cannot be recovered.**
|
|
350
404
|
|
|
351
|
-
- Reuse it
|
|
405
|
+
- Reuse it with `publish_site`, `publish_json`, or `publish_kv` to group new resources under one owner; REST creates accept `X-Owner-Token`.
|
|
352
406
|
- Pass it to `delete_resource` to remove content.
|
|
353
|
-
- Pass it to `
|
|
407
|
+
- Pass it to `search_gallery` with `mine=true` to list owned resources. `get_page` has no owner-token input; protected reads use `password`.
|
|
354
408
|
|
|
355
409
|
## Configuration
|
|
356
410
|
|
|
357
411
|
| Variable | Default | Description |
|
|
358
412
|
|----------|---------|-------------|
|
|
359
|
-
| `BREWPAGE_URL` | `https://brewpage.app` | API base URL |
|
|
413
|
+
| `BREWPAGE_URL` | `https://brewpage.app` | Authorized apex API base URL; never a publication host |
|
|
360
414
|
|
|
361
415
|
## Links
|
|
362
416
|
|
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.8.0",
|
|
9
9
|
});
|
|
10
10
|
const PUBLIC_NAMESPACE = "public";
|
|
11
11
|
/**
|
|
@@ -30,12 +30,26 @@ 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
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
async function apiRequest(method, path, body, headers, responseType = "json") {
|
|
39
53
|
const url = `${BASE_URL}${path}`;
|
|
40
54
|
const res = await fetch(url, {
|
|
41
55
|
method,
|
|
@@ -45,24 +59,25 @@ async function apiRequest(method, path, body, headers) {
|
|
|
45
59
|
},
|
|
46
60
|
body: body ? JSON.stringify(body) : undefined,
|
|
47
61
|
});
|
|
48
|
-
const data = await res.json().catch(() => null);
|
|
49
|
-
return { ok: res.ok, status: res.status, data };
|
|
62
|
+
const data = responseType === "text" ? await res.text() : await res.json().catch(() => null);
|
|
63
|
+
return { ok: res.ok, status: res.status, data, headers: res.headers };
|
|
50
64
|
}
|
|
51
65
|
function formatPublishResponse(data, effectiveNamespace) {
|
|
52
66
|
const lines = [
|
|
53
67
|
`Published successfully!`,
|
|
54
68
|
``,
|
|
55
|
-
`URL: ${data.
|
|
69
|
+
`URL: ${data.link || data.url || "N/A"}`,
|
|
56
70
|
];
|
|
57
71
|
if (data.shortUrl)
|
|
58
72
|
lines.push(`Short URL: ${data.shortUrl}`);
|
|
59
73
|
lines.push(`Namespace: ${data.namespace || "N/A"}`, `ID: ${data.id || "N/A"}`);
|
|
60
74
|
if (data.expiresAt)
|
|
61
75
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
76
|
+
lines.push(...hostingResponseLines(data));
|
|
62
77
|
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 = (
|
|
78
|
+
const ns = (data.namespace ?? effectiveNamespace) || "";
|
|
64
79
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
65
|
-
lines.push(``, UNLISTED_NOTICE);
|
|
80
|
+
lines.push(``, data.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
66
81
|
}
|
|
67
82
|
return lines.join("\n");
|
|
68
83
|
}
|
|
@@ -70,7 +85,7 @@ function formatUpdateResponse(data) {
|
|
|
70
85
|
const lines = [
|
|
71
86
|
`Updated successfully!`,
|
|
72
87
|
``,
|
|
73
|
-
`URL: ${data.
|
|
88
|
+
`URL: ${data.link || data.url || "N/A"}`,
|
|
74
89
|
];
|
|
75
90
|
if (data.shortUrl)
|
|
76
91
|
lines.push(`Short URL: ${data.shortUrl}`);
|
|
@@ -79,10 +94,12 @@ function formatUpdateResponse(data) {
|
|
|
79
94
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
80
95
|
if (data.updatedAt)
|
|
81
96
|
lines.push(`Updated: ${data.updatedAt}`);
|
|
97
|
+
lines.push(...hostingResponseLines(data));
|
|
82
98
|
return lines.join("\n");
|
|
83
99
|
}
|
|
84
100
|
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).", {
|
|
85
101
|
content: z.string().describe("HTML or Markdown content to publish"),
|
|
102
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
86
103
|
format: z
|
|
87
104
|
.enum(["HTML", "MARKDOWN"])
|
|
88
105
|
.default("HTML")
|
|
@@ -111,19 +128,22 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
|
|
|
111
128
|
.boolean()
|
|
112
129
|
.optional()
|
|
113
130
|
.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 }) => {
|
|
131
|
+
}, async ({ content, format, namespace, password, ttlDays, filename, showTopBar, deliveryMode }) => {
|
|
115
132
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (ttlDays)
|
|
121
|
-
body.ttlDays = ttlDays;
|
|
133
|
+
const qsParams = [`ns=${encodeURIComponent(effectiveNamespace)}`, `format=${encodeURIComponent(format.toLowerCase())}`];
|
|
134
|
+
if (ttlDays !== undefined)
|
|
135
|
+
qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
|
|
136
|
+
const body = { content };
|
|
122
137
|
if (filename)
|
|
123
138
|
body.filename = filename;
|
|
124
139
|
if (showTopBar !== undefined)
|
|
125
140
|
body.showTopBar = showTopBar;
|
|
126
|
-
const
|
|
141
|
+
const headers = {};
|
|
142
|
+
if (password)
|
|
143
|
+
headers["X-Password"] = password;
|
|
144
|
+
if (deliveryMode)
|
|
145
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
146
|
+
const { ok, data } = await apiRequest("POST", `/api/html?${qsParams.join("&")}`, body, headers);
|
|
127
147
|
if (!ok) {
|
|
128
148
|
return {
|
|
129
149
|
content: [
|
|
@@ -146,12 +166,13 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
|
|
|
146
166
|
});
|
|
147
167
|
server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public URL and owner token.", {
|
|
148
168
|
url: z.string().url().describe("URL of the file to upload"),
|
|
169
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
149
170
|
namespace: z
|
|
150
171
|
.string()
|
|
151
172
|
.optional()
|
|
152
173
|
.describe(NAMESPACE_DESCRIPTION),
|
|
153
174
|
filename: z.string().optional().describe("Custom filename (optional)"),
|
|
154
|
-
}, async ({ url: fileUrl, namespace, filename }) => {
|
|
175
|
+
}, async ({ url: fileUrl, namespace, filename, deliveryMode }) => {
|
|
155
176
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
156
177
|
const fileRes = await fetch(fileUrl);
|
|
157
178
|
if (!fileRes.ok) {
|
|
@@ -168,9 +189,9 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
|
|
|
168
189
|
const blob = await fileRes.blob();
|
|
169
190
|
const formData = new FormData();
|
|
170
191
|
formData.append("file", blob, filename || fileUrl.split("/").pop() || "file");
|
|
171
|
-
|
|
172
|
-
const res = await fetch(`${BASE_URL}/api/files`, {
|
|
192
|
+
const res = await fetch(`${BASE_URL}/api/files?ns=${encodeURIComponent(effectiveNamespace)}`, {
|
|
173
193
|
method: "POST",
|
|
194
|
+
headers: deliveryMode ? { "X-Delivery-Mode": deliveryMode } : undefined,
|
|
174
195
|
body: formData,
|
|
175
196
|
});
|
|
176
197
|
const data = await res.json().catch(() => null);
|
|
@@ -194,6 +215,27 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
|
|
|
194
215
|
],
|
|
195
216
|
};
|
|
196
217
|
});
|
|
218
|
+
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.", {
|
|
219
|
+
type: z.enum(["html", "file", "json", "kv"]).describe("Resource type; sites have no hosting-choice endpoint"),
|
|
220
|
+
namespace: z.string().describe("Resource namespace; only public is eligible"),
|
|
221
|
+
id: z.string().describe("Resource ID"),
|
|
222
|
+
ownerToken: z.string().describe("Original resource owner token"),
|
|
223
|
+
deliveryMode: z.enum(["path", "subdomain"]).describe("path = Promotion; subdomain = Dedicated subdomain"),
|
|
224
|
+
expectedVersion: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).describe("Current hostingVersion from the server; required for concurrency control"),
|
|
225
|
+
}, async ({ type, namespace, id, ownerToken, deliveryMode, expectedVersion }) => {
|
|
226
|
+
const segment = type === "file" ? "files" : type;
|
|
227
|
+
const path = `/api/${segment}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/hosting`;
|
|
228
|
+
const { ok, status, data } = await apiRequest("PATCH", path, { deliveryMode, expectedVersion }, { "X-Owner-Token": ownerToken });
|
|
229
|
+
if (!ok) {
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: `Failed to update hosting (${status}): ${JSON.stringify(data)}` }],
|
|
232
|
+
isError: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: formatUpdateResponse(data) }],
|
|
237
|
+
};
|
|
238
|
+
});
|
|
197
239
|
server.tool("delete_resource", "Delete a BrewPage resource (HTML page, KV store, JSON collection, or file) using the owner token.", {
|
|
198
240
|
type: z
|
|
199
241
|
.enum(["html", "kv", "json", "file"])
|
|
@@ -242,27 +284,35 @@ server.tool("get_page", "Fetch a published BrewPage HTML page content by namespa
|
|
|
242
284
|
const headers = {};
|
|
243
285
|
if (password)
|
|
244
286
|
headers["X-Password"] = password;
|
|
245
|
-
const { ok, status, data } = await apiRequest("GET", `/api/html/${namespace}/${id}`, undefined, headers);
|
|
287
|
+
const { ok, status, data, headers: responseHeaders } = await apiRequest("GET", `/api/html/${namespace}/${id}`, undefined, headers, "text");
|
|
246
288
|
if (!ok) {
|
|
289
|
+
let errorData = data;
|
|
290
|
+
try {
|
|
291
|
+
errorData = JSON.parse(data);
|
|
292
|
+
}
|
|
293
|
+
catch { }
|
|
247
294
|
return {
|
|
248
295
|
content: [
|
|
249
296
|
{
|
|
250
297
|
type: "text",
|
|
251
|
-
text: `Failed to fetch page (${status}): ${JSON.stringify(
|
|
298
|
+
text: `Failed to fetch page (${status}): ${JSON.stringify(errorData)}`,
|
|
252
299
|
},
|
|
253
300
|
],
|
|
254
301
|
isError: true,
|
|
255
302
|
};
|
|
256
303
|
}
|
|
257
|
-
const
|
|
304
|
+
const routingCohort = responseHeaders.get("X-Routing-Cohort");
|
|
305
|
+
const deliveryMode = responseHeaders.get("X-Delivery-Mode");
|
|
306
|
+
const hosting = { ...(routingCohort !== null ? { routingCohort } : {}), ...(deliveryMode !== null ? { deliveryMode } : {}) };
|
|
307
|
+
const hasNewMarkers = (routingCohort !== null && routingCohort !== "old") || deliveryMode !== null;
|
|
258
308
|
const lines = [
|
|
259
309
|
`Page: ${namespace}/${id}`,
|
|
260
|
-
`URL: ${
|
|
261
|
-
`
|
|
262
|
-
|
|
310
|
+
`URL: ${responseHeaders.get("X-Canonical-Link") ?? (hasNewMarkers ? "N/A" : `${BASE_URL}/${namespace}/${id}`)}`,
|
|
311
|
+
`Content-Type: ${responseHeaders.get("Content-Type") || "N/A"}`,
|
|
312
|
+
...hostingResponseLines(hosting),
|
|
263
313
|
``,
|
|
264
314
|
`--- Content ---`,
|
|
265
|
-
|
|
315
|
+
data,
|
|
266
316
|
];
|
|
267
317
|
return {
|
|
268
318
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -297,6 +347,7 @@ server.tool("get_stats", "Get BrewPage platform-wide usage statistics including
|
|
|
297
347
|
};
|
|
298
348
|
});
|
|
299
349
|
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`.", {
|
|
350
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
300
351
|
entryContent: z
|
|
301
352
|
.string()
|
|
302
353
|
.optional()
|
|
@@ -327,7 +378,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
327
378
|
.string()
|
|
328
379
|
.optional()
|
|
329
380
|
.describe("Existing owner token to group this site under the same owner as previous content"),
|
|
330
|
-
}, async ({ entryContent, files, entry, namespace, password, ttlDays, ownerToken }) => {
|
|
381
|
+
}, async ({ entryContent, files, entry, namespace, password, ttlDays, ownerToken, deliveryMode }) => {
|
|
331
382
|
const hasEntry = typeof entryContent === "string";
|
|
332
383
|
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
333
384
|
if (hasEntry === hasFiles) {
|
|
@@ -359,6 +410,8 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
359
410
|
headers["X-Password"] = password;
|
|
360
411
|
if (ownerToken)
|
|
361
412
|
headers["X-Owner-Token"] = ownerToken;
|
|
413
|
+
if (deliveryMode)
|
|
414
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
362
415
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
363
416
|
const qsParams = [];
|
|
364
417
|
if (entry)
|
|
@@ -399,10 +452,11 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
399
452
|
lines.push(`Tags: ${site.tags.join(", ")}`);
|
|
400
453
|
if (site.expiresAt)
|
|
401
454
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
455
|
+
lines.push(...hostingResponseLines(site));
|
|
402
456
|
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
457
|
const ns = (site.namespace || effectiveNamespace) || "";
|
|
404
458
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
405
|
-
lines.push(``, UNLISTED_NOTICE);
|
|
459
|
+
lines.push(``, site.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
406
460
|
}
|
|
407
461
|
return {
|
|
408
462
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -506,6 +560,7 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
506
560
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
507
561
|
if (site.result)
|
|
508
562
|
lines.push(`Result: ${site.result}`);
|
|
563
|
+
lines.push(...hostingResponseLines(site));
|
|
509
564
|
if (site.visibilityNotice)
|
|
510
565
|
lines.push(``, String(site.visibilityNotice));
|
|
511
566
|
return {
|
|
@@ -552,6 +607,7 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
552
607
|
};
|
|
553
608
|
});
|
|
554
609
|
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.", {
|
|
610
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
555
611
|
json: z
|
|
556
612
|
.union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
|
|
557
613
|
.describe("JSON document to publish (object, array, or JSON-encoded string)"),
|
|
@@ -578,7 +634,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
|
|
|
578
634
|
.string()
|
|
579
635
|
.optional()
|
|
580
636
|
.describe("Existing owner token to group this document under the same owner as previous content"),
|
|
581
|
-
}, async ({ json, namespace, password, ttlDays, tags, ownerToken }) => {
|
|
637
|
+
}, async ({ json, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
582
638
|
const jsonString = typeof json === "string" ? json : JSON.stringify(json);
|
|
583
639
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
584
640
|
const qsParams = [];
|
|
@@ -595,6 +651,8 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
|
|
|
595
651
|
headers["X-Password"] = password;
|
|
596
652
|
if (ownerToken)
|
|
597
653
|
headers["X-Owner-Token"] = ownerToken;
|
|
654
|
+
if (deliveryMode)
|
|
655
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
598
656
|
const res = await fetch(`${BASE_URL}/api/json${qs}`, {
|
|
599
657
|
method: "POST",
|
|
600
658
|
headers,
|
|
@@ -696,6 +754,7 @@ server.tool("update_json", "Update an existing BrewPage JSON document. Requires
|
|
|
696
754
|
};
|
|
697
755
|
});
|
|
698
756
|
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.", {
|
|
757
|
+
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
699
758
|
key: z.string().describe("KV key (first part of the (namespace, id, key) addressing tuple)"),
|
|
700
759
|
value: z.string().describe("KV value"),
|
|
701
760
|
namespace: z
|
|
@@ -721,7 +780,7 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
721
780
|
.string()
|
|
722
781
|
.optional()
|
|
723
782
|
.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 }) => {
|
|
783
|
+
}, async ({ key, value, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
725
784
|
const body = { key, value };
|
|
726
785
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
727
786
|
const qsParams = [];
|
|
@@ -738,6 +797,8 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
738
797
|
headers["X-Password"] = password;
|
|
739
798
|
if (ownerToken)
|
|
740
799
|
headers["X-Owner-Token"] = ownerToken;
|
|
800
|
+
if (deliveryMode)
|
|
801
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
741
802
|
const { ok, status, data } = await apiRequest("POST", `/api/kv${qs}`, body, headers);
|
|
742
803
|
if (!ok) {
|
|
743
804
|
return {
|
package/package.json
CHANGED