brewpage-mcp 1.12.1 → 1.13.1
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 +65 -17
- package/dist/index.js +259 -148
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/brewpage-mcp)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://mcprush.com/to/brewpage-mcp?ref=badge)
|
|
5
6
|
|
|
6
7
|
MCP server for [BrewPage](https://brewpage.app) -- publish and manage HTML, KV, JSON, and file content directly from AI assistants.
|
|
7
8
|
|
|
8
9
|
## What is BrewPage
|
|
9
10
|
|
|
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
|
|
11
|
+
[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 account needed, 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 twenty 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.
|
|
12
|
+
|
|
13
|
+
Optional: sign in free (Google or any email) at https://account.brewpage.app to never lose your pages or tokens and to mint tokens that never expire. Not needed to publish.
|
|
11
14
|
|
|
12
15
|
Choosing a host? See how BrewPage compares with GitHub Pages, Netlify Drop, Tiiny Host and other tools: [brewpage.app/compare](https://brewpage.app/compare).
|
|
13
16
|
|
|
14
17
|
## Quick Start
|
|
15
18
|
|
|
16
19
|
```bash
|
|
17
|
-
npx -y brewpage-mcp@1.
|
|
20
|
+
npx -y brewpage-mcp@1.13.1
|
|
18
21
|
```
|
|
19
22
|
|
|
20
23
|
Requires Node.js 20 or newer. This starts the stdio server for an MCP client.
|
|
@@ -30,7 +33,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
30
33
|
"mcpServers": {
|
|
31
34
|
"brewpage": {
|
|
32
35
|
"command": "npx",
|
|
33
|
-
"args": ["-y", "brewpage-mcp@1.
|
|
36
|
+
"args": ["-y", "brewpage-mcp@1.13.1"]
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -41,7 +44,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
41
44
|
Register a project-scoped stdio server:
|
|
42
45
|
|
|
43
46
|
```bash
|
|
44
|
-
claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.
|
|
47
|
+
claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.13.1
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
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).
|
|
@@ -55,7 +58,7 @@ Open **Settings > MCP** and add:
|
|
|
55
58
|
"mcpServers": {
|
|
56
59
|
"brewpage": {
|
|
57
60
|
"command": "npx",
|
|
58
|
-
"args": ["-y", "brewpage-mcp@1.
|
|
61
|
+
"args": ["-y", "brewpage-mcp@1.13.1"]
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
}
|
|
@@ -69,7 +72,7 @@ Open the Cline MCP settings panel and add:
|
|
|
69
72
|
{
|
|
70
73
|
"brewpage": {
|
|
71
74
|
"command": "npx",
|
|
72
|
-
"args": ["-y", "brewpage-mcp@1.
|
|
75
|
+
"args": ["-y", "brewpage-mcp@1.13.1"]
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
```
|
|
@@ -77,13 +80,13 @@ Open the Cline MCP settings panel and add:
|
|
|
77
80
|
### Global Install
|
|
78
81
|
|
|
79
82
|
```bash
|
|
80
|
-
npm install -g brewpage-mcp@1.
|
|
83
|
+
npm install -g brewpage-mcp@1.13.1
|
|
81
84
|
brewpage-mcp
|
|
82
85
|
```
|
|
83
86
|
|
|
84
87
|
## Tools
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
Twenty tools are available. Creation returns an owner token; keep it to modify or delete the resource later. Owner tools take `ownerToken`, else `BREWPAGE_OWNER_TOKEN`. Supported password inputs use `X-Password`. Secrets allowed; likely ones are flagged in notices. For a secret: private namespace + password.
|
|
87
90
|
|
|
88
91
|
| Tool | Purpose |
|
|
89
92
|
|------|---------|
|
|
@@ -100,14 +103,17 @@ Seventeen tools are available. Creation returns an owner token; keep it to modif
|
|
|
100
103
|
| `set_kv` | Set or replace a key in an owned KV store |
|
|
101
104
|
| `get_kv` | Read a KV value |
|
|
102
105
|
| `update_hosting` | Change eligible delivery mode with owner token and hosting version |
|
|
103
|
-
| `delete_resource` | Delete HTML, JSON, KV
|
|
106
|
+
| `delete_resource` | Delete an HTML page, site, JSON document, KV store or file |
|
|
107
|
+
| `set_visibility` | Hide or show a public publication in gallery & search; link unchanged |
|
|
108
|
+
| `bind_token` | Add an owner token to your profile (admin token) |
|
|
109
|
+
| `mint_token` | Mint a profile token, shown once (admin token) |
|
|
104
110
|
| `search_gallery` | Browse public content or list owned resources |
|
|
105
111
|
| `get_stats` | Read platform-wide statistics |
|
|
106
112
|
| `get_limits` | Read the current upload limits (`GET /api/limits`) |
|
|
107
113
|
|
|
108
114
|
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 and unlisted, protected or otherwise ineligible native responses carry `noindex`; eligible public HTML, sites and files may permit indexing.
|
|
109
115
|
|
|
110
|
-
The MCP server wraps a subset of the backend-generated public [REST contract](../openapi/openapi.json). TTL inputs take days or a string such as `2w`/`36h` (default 15); the server rounds to whole days and clamps to 1–30 (owner-specific caps and eligible automatic extensions can differ). Use returned `expiresAt`. Every tolerated-input adjustment (TTL clamp, format alias, skipped site member, normalised namespace) comes back in the response `notices` array and is printed as `Notice (code): message`. `get_page` reads stored rendered HTML or format-specific code/text from the API; it does not call the owner-only original-source endpoint or a browser viewer. `publish_file` uploads from a source URL and does not expose password or TTL inputs.
|
|
116
|
+
The MCP server wraps a subset of the backend-generated public [REST contract](../openapi/openapi.json). TTL inputs take days or a string such as `2w`/`36h` (default 15); the server rounds to whole days and clamps to 1–30 (owner-specific caps and eligible automatic extensions can differ). Use returned `expiresAt`. Every tolerated-input adjustment (TTL clamp, format alias, skipped site member, normalised namespace) comes back in the response `notices` array and is printed as `Notice (code): message`; `secret-detected` and `manage-link-disabled` print as `WARNING (code): message`: relay them to the user. A minted `manageUrl` prints as `MANAGE LINK (private): …`: give it to the user privately, never publish it. A listed public result prints `VISIBILITY: public, listed. Hide: set_visibility listed=false`. `get_page` reads stored rendered HTML or format-specific code/text from the API; it does not call the owner-only original-source endpoint or a browser viewer. `publish_file` uploads from a source URL and does not expose password or TTL inputs.
|
|
111
117
|
|
|
112
118
|
For a fresh publication, sites and HTML pages of format `HTML` outside `public` (including the generated unlisted namespace) always use Dedicated subdomain (`subdomain`); omit `deliveryMode` there. An explicit `path` there is not rejected: the publication is created on its subdomain and the result reports `deliveryModeMatched: false`, `deliveryModeReason: "policy:subdomain-required"` and a `Delivery notice:` line. Everything else in any namespace (Markdown, code/text, files, JSON, KV, public HTML) defaults to Promotion (`path`) and may choose Dedicated subdomain, including password-protected content. Promotion uses a BrewPage path with stricter active-document restrictions. Each NEW site gets one publication-specific host for its complete bundle, preserving relative/root-relative paths; missing assets return 404, with no SPA fallback. Third-party widgets may still need provider configuration.
|
|
113
119
|
|
|
@@ -341,7 +347,7 @@ get_kv(namespace="public", id="aBcDeFgHiJ", key="theme")
|
|
|
341
347
|
|
|
342
348
|
Browse the public gallery of BrewPage pages, or list resources you own. Returns a paged result with metadata (id, title, view count, created date). All parameters are optional; with no parameters this returns the most recent public pages.
|
|
343
349
|
|
|
344
|
-
Parameters: `q` (optional search query), `page` (optional,
|
|
350
|
+
Parameters: `q` (optional search query), `page` (optional, 1-based), `size` (optional page size), `sort` (optional `date` | `views`), `mine` (optional boolean -- list only resources owned by `ownerToken`), `ownerToken` (required only when `mine=true`).
|
|
345
351
|
|
|
346
352
|
Example prompt:
|
|
347
353
|
|
|
@@ -357,11 +363,9 @@ search_gallery(mine=true, ownerToken="tok_...", sort="date")
|
|
|
357
363
|
|
|
358
364
|
### `delete_resource`
|
|
359
365
|
|
|
360
|
-
Delete a BrewPage resource (HTML page, KV store, JSON document, or file) using the owner token received at creation.
|
|
361
|
-
|
|
362
|
-
Parameters: `type` (`html` | `kv` | `json` | `file`), `namespace` (string), `id` (string), `ownerToken` (string).
|
|
366
|
+
Delete a BrewPage resource (HTML page, site, KV store, JSON document, or file) using the owner token received at creation.
|
|
363
367
|
|
|
364
|
-
|
|
368
|
+
Parameters: `type` (`html` | `site` | `kv` | `json` | `file`), `namespace` (string), `id` (string), `ownerToken` (optional; default `BREWPAGE_OWNER_TOKEN`). Path segments are URL-encoded.
|
|
365
369
|
|
|
366
370
|
Example prompt:
|
|
367
371
|
|
|
@@ -373,6 +377,36 @@ delete_resource(type="html", namespace="public", id="aBcDeFgHiJ", ownerToken="to
|
|
|
373
377
|
|
|
374
378
|
---
|
|
375
379
|
|
|
380
|
+
### `set_visibility`
|
|
381
|
+
|
|
382
|
+
Hide or show a public publication in gallery & search; the link stays the same. Only public, password-free HTML, sites and files can be listed; others answer with a `visibility-not-listable` notice.
|
|
383
|
+
|
|
384
|
+
Parameters: `type` (`html` | `site` | `file` | `json` | `kv`), `namespace`, `id`, `listed` (boolean), `ownerToken` (optional).
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
set_visibility(type="html", namespace="public", id="aBcDeFgHiJ", listed=false)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Output: `LISTED: false · link unchanged`.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### `bind_token`
|
|
395
|
+
|
|
396
|
+
Add an owner token to your BrewPage profile (as `publish`) with an `admin` token of that profile (`POST /api/owner-token/bind`). Output: `BOUND …ab12 · 3/20 active`; tokens are never printed in full.
|
|
397
|
+
|
|
398
|
+
Parameters: `token`, `ownerToken` (admin token; optional, default `BREWPAGE_OWNER_TOKEN`).
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### `mint_token`
|
|
403
|
+
|
|
404
|
+
Mint a new profile token with an `admin` token (`POST /api/owner-token/mint`). The new token is shown once; the admin token is never printed.
|
|
405
|
+
|
|
406
|
+
Parameters: `label` (optional), `scope` (`publish` default | `admin`), `expiresIn` (`30d` | `90d` | `1y` | `never`; default admin 90d, publish never), `ownerToken` (admin token; optional).
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
376
410
|
### `get_stats`
|
|
377
411
|
|
|
378
412
|
Get platform-wide BrewPage usage statistics (page count, file count, storage, daily totals). Supports an optional IANA timezone for the "today" boundary.
|
|
@@ -391,7 +425,7 @@ get_stats()
|
|
|
391
425
|
|
|
392
426
|
### `get_limits`
|
|
393
427
|
|
|
394
|
-
Read the current upload limits from `GET /api/limits` (no auth). Sizes are bytes: `requestMaxBytes` plus `sites.maxFiles`, `maxFileBytes`, `maxTotalBytes` (uncompressed), `maxArchiveExpansionBytes`, `maxPathChars`, `maxPathBytes`, `maxNameBytes`, `defaultTtlDays`, `maxTtlDays`, `allowedExtensions`, `skippedFileNames` and `skippedRootFolders`. Call it before publishing a large or multi-file site.
|
|
428
|
+
Read the current upload limits from `GET /api/limits` (no auth; with `BREWPAGE_OWNER_TOKEN` also `reach` and `ownerMaxTtlDays`). Sizes are bytes: `requestMaxBytes` plus `sites.maxFiles`, `maxFileBytes`, `maxTotalBytes` (uncompressed), `maxArchiveExpansionBytes`, `maxPathChars`, `maxPathBytes`, `maxNameBytes`, `defaultTtlDays`, `maxTtlDays`, `allowedExtensions`, `skippedFileNames` and `skippedRootFolders`. Call it before publishing a large or multi-file site.
|
|
395
429
|
|
|
396
430
|
Parameters: none.
|
|
397
431
|
|
|
@@ -432,7 +466,8 @@ get_limits()
|
|
|
432
466
|
|
|
433
467
|
Every publish response includes an **owner token** -- the only credential that allows updating or deleting your content. **Save it. It cannot be recovered.**
|
|
434
468
|
|
|
435
|
-
- Reuse it with
|
|
469
|
+
- Reuse it with any publish tool (`ownerToken`) to group new resources under one owner; REST creates accept `X-Owner-Token`.
|
|
470
|
+
- Set `BREWPAGE_OWNER_TOKEN` to use one token for every call; the output then names it instead of printing it.
|
|
436
471
|
- Pass it to `delete_resource` to remove content.
|
|
437
472
|
- Pass it to `search_gallery` with `mine=true` to list owned resources. `get_page` has no owner-token input; protected reads use `password`.
|
|
438
473
|
|
|
@@ -441,6 +476,7 @@ Every publish response includes an **owner token** -- the only credential that a
|
|
|
441
476
|
| Variable | Default | Description |
|
|
442
477
|
|----------|---------|-------------|
|
|
443
478
|
| `BREWPAGE_URL` | `https://brewpage.app` | Authorized apex API base URL; never a publication host |
|
|
479
|
+
| `BREWPAGE_OWNER_TOKEN` | unset | Optional default owner token (32 letters/digits) for every call; unset = new token per create |
|
|
444
480
|
|
|
445
481
|
## Links
|
|
446
482
|
|
|
@@ -455,6 +491,18 @@ Every publish response includes an **owner token** -- the only credential that a
|
|
|
455
491
|
|
|
456
492
|
## Changelog
|
|
457
493
|
|
|
494
|
+
## 1.13.1 -- 2026-10-25
|
|
495
|
+
|
|
496
|
+
- Old publications (created before the new link format): update tools say they answer `409 OLD_PUBLICATION_READ_ONLY`; that failure adds a hint to publish the content anew and give the user the new URL and owner token.
|
|
497
|
+
|
|
498
|
+
## 1.13.0 -- 2026-10-25
|
|
499
|
+
|
|
500
|
+
- Identifying `User-Agent: brewpage-mcp/1.13.0` on every BrewPage call.
|
|
501
|
+
- Optional `BREWPAGE_OWNER_TOKEN` default; `ownerToken` on `publish_html` and `publish_file`; owner tools fail before HTTP without a token.
|
|
502
|
+
- New tools: `set_visibility`, `bind_token`, `mint_token` (twenty tools).
|
|
503
|
+
- `delete_resource` deletes sites and URL-encodes path segments.
|
|
504
|
+
- Prints `MANAGE LINK (private)`, `VISIBILITY` and `WARNING (code)` lines; a wrong-token 403 adds a hint.
|
|
505
|
+
|
|
458
506
|
## 1.12.1 -- 2026-10-22
|
|
459
507
|
|
|
460
508
|
- Package README links the BrewPage comparison page (https://brewpage.app/compare). Same seventeen tools, inputs and HTTP behavior.
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,68 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
const BASE_URL = process.env.BREWPAGE_URL || "https://brewpage.app";
|
|
6
|
+
const USER_AGENT = "brewpage-mcp/1.13.1";
|
|
6
7
|
const server = new McpServer({
|
|
7
8
|
name: "brewpage-mcp",
|
|
8
|
-
version: "1.
|
|
9
|
+
version: "1.13.1",
|
|
9
10
|
});
|
|
11
|
+
const OWNER_TOKEN_SHAPE = /^[A-Za-z0-9]{32}$/;
|
|
12
|
+
const OWNER_TOKEN_ENV = "BREWPAGE_OWNER_TOKEN";
|
|
13
|
+
/** Optional default owner token for every call; a malformed value is ignored with one stderr line. */
|
|
14
|
+
const ENV_OWNER_TOKEN = (() => {
|
|
15
|
+
const raw = process.env[OWNER_TOKEN_ENV]?.trim();
|
|
16
|
+
if (!raw)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (OWNER_TOKEN_SHAPE.test(raw))
|
|
19
|
+
return raw;
|
|
20
|
+
console.error(`${OWNER_TOKEN_ENV} ignored: expected 32 letters/digits.`);
|
|
21
|
+
return undefined;
|
|
22
|
+
})();
|
|
23
|
+
const NO_OWNER_TOKEN = `No owner token: pass ownerToken or set ${OWNER_TOKEN_ENV}.`;
|
|
24
|
+
const CREATE_OWNER_TOKEN_DESCRIPTION = `Optional. Default: ${OWNER_TOKEN_ENV}, else a new token per create.`;
|
|
25
|
+
const OWNER_TOKEN_DESCRIPTION = `Owner token from create. Default: ${OWNER_TOKEN_ENV}.`;
|
|
26
|
+
const SECRETS = "Secrets allowed; likely ones are flagged in notices. For a secret: private namespace + password.";
|
|
27
|
+
const MANAGE_URL_TEXT = "Returns manageUrl (private): give it to the user privately; never publish it.";
|
|
28
|
+
const OLD_READ_ONLY_CODE = "OLD_PUBLICATION_READ_ONLY";
|
|
29
|
+
const OLD_READ_ONLY_TEXT = ` Old publications (created before the new link format) answer 409 ${OLD_READ_ONLY_CODE}.`;
|
|
30
|
+
/** Notice codes the agent must relay to the user. */
|
|
31
|
+
const WARNING_CODES = new Set(["secret-detected", "manage-link-disabled"]);
|
|
32
|
+
/** MCP type name -> REST path segment. */
|
|
33
|
+
const TYPE_SEGMENTS = { html: "html", site: "sites", file: "files", json: "json", kv: "kv" };
|
|
34
|
+
/** Caller argument first, then the configured default; `fromEnv` marks the configured one. */
|
|
35
|
+
function ownerTokenFor(argument) {
|
|
36
|
+
if (argument && argument.length > 0)
|
|
37
|
+
return { token: argument, fromEnv: false };
|
|
38
|
+
return { token: ENV_OWNER_TOKEN, fromEnv: ENV_OWNER_TOKEN !== undefined };
|
|
39
|
+
}
|
|
40
|
+
/** Only the last 4 characters: tokens never reach a transcript in full. */
|
|
41
|
+
function maskToken(token) {
|
|
42
|
+
return `…${token.slice(-4)}`;
|
|
43
|
+
}
|
|
44
|
+
function textResult(text, isError = false) {
|
|
45
|
+
return isError ? { content: [{ type: "text", text }], isError: true } : { content: [{ type: "text", text }] };
|
|
46
|
+
}
|
|
47
|
+
/** Owner-tool failure: the server answer verbatim plus a hint for a plain wrong-token 403 or a frozen old publication. */
|
|
48
|
+
function ownerFailure(operation, status, data, fromEnv) {
|
|
49
|
+
const lines = [`Failed to ${operation} (${status}): ${JSON.stringify(data)}`];
|
|
50
|
+
const code = data?.code;
|
|
51
|
+
if (status === 403 && (code === undefined || code === "OWNER_AUTHORIZATION_FAILED")) {
|
|
52
|
+
lines.push(`Wrong owner token for this publication. Use ownerToken from create.${fromEnv ? ` Sent: ${OWNER_TOKEN_ENV}.` : ""}`);
|
|
53
|
+
}
|
|
54
|
+
if (status === 409 && code === OLD_READ_ONLY_CODE) {
|
|
55
|
+
lines.push("Read-only old publication: cannot be updated. Publish the content anew with the matching " +
|
|
56
|
+
"publish tool in the same namespace; give the user the NEW URL and owner token. The old URL keeps its old content " +
|
|
57
|
+
"until it expires. Ignore republishUrl (browser-only)." +
|
|
58
|
+
(operation === "set KV" ? " For KV, read all keys with get_kv and publish them together with publish_kv." : ""));
|
|
59
|
+
}
|
|
60
|
+
return textResult(lines.join("\n"), true);
|
|
61
|
+
}
|
|
62
|
+
/** Every BrewPage call identifies the client; source-file downloads use plain fetch. */
|
|
63
|
+
function bpFetch(url, init = {}) {
|
|
64
|
+
const headers = new Headers(init.headers);
|
|
65
|
+
headers.set("User-Agent", USER_AGENT);
|
|
66
|
+
return fetch(url, { ...init, headers });
|
|
67
|
+
}
|
|
10
68
|
const PUBLIC_NAMESPACE = "public";
|
|
11
69
|
/**
|
|
12
70
|
* Generate a short, unlisted private namespace slug used when the caller omits
|
|
@@ -29,13 +87,13 @@ function defaultPrivateNamespace() {
|
|
|
29
87
|
function resolveNamespace(namespace) {
|
|
30
88
|
return namespace && namespace.length > 0 ? namespace : defaultPrivateNamespace();
|
|
31
89
|
}
|
|
32
|
-
const UNLISTED_NOTICE = "
|
|
33
|
-
|
|
34
|
-
const PROTECTED_UNLISTED_NOTICE = "Password-protected unlisted link — readers need the password; excluded from gallery and search.";
|
|
90
|
+
const UNLISTED_NOTICE = "Private: not listed. Anyone with the link can open it. Use namespace public for gallery & search.";
|
|
91
|
+
const PROTECTED_UNLISTED_NOTICE = "Password: link + password. Not listed.";
|
|
35
92
|
const NAMESPACE_DESCRIPTION = "Optional. Omit to generate an unlisted namespace: anyone with its unpassworded link can read it, " +
|
|
36
93
|
"but it is excluded from gallery/search. Pass a custom namespace to group unlisted content, " +
|
|
37
94
|
"or pass `public` ONLY when the user explicitly wants gallery/search eligibility. " +
|
|
38
|
-
"Password-protected content stays excluded; unlisted, protected or otherwise ineligible native responses carry noindex; eligible public HTML, sites and files may permit indexing."
|
|
95
|
+
"Password-protected content stays excluded; unlisted, protected or otherwise ineligible native responses carry noindex; eligible public HTML, sites and files may permit indexing. " +
|
|
96
|
+
"Lowercase only: 1-32 of a-z, 0-9, hyphen.";
|
|
39
97
|
const TTL_SCHEMA = z.union([z.number().positive(), z.string().min(1).max(32)]).optional().describe("Requested TTL: days as a number, or a string such as '2w', '36h' or '1 month' (default 15 days). " +
|
|
40
98
|
"The server rounds to whole days and clamps to the allowed range (1-30 by default; owner caps differ), reporting each adjustment in notices; use returned expiresAt.");
|
|
41
99
|
const DELIVERY_MODE_SCHEMA = z.enum(["path", "subdomain"]).optional().describe("Optional delivery choice for a fresh publication, independent of password/namespace. " +
|
|
@@ -66,7 +124,38 @@ function noticeLines(data) {
|
|
|
66
124
|
return [];
|
|
67
125
|
return data.notices
|
|
68
126
|
.filter((n) => typeof n === "object" && n !== null && typeof n.code === "string" && typeof n.message === "string")
|
|
69
|
-
.map((n) =>
|
|
127
|
+
.map((n) => `${WARNING_CODES.has(n.code) ? "WARNING" : "Notice"} (${n.code}): ${n.message}`);
|
|
128
|
+
}
|
|
129
|
+
const SIGN_IN_HINT = "Optional: sign in free (Google or any email) at https://account.brewpage.app to never lose your pages or tokens and to mint tokens that never expire. Not needed to publish.";
|
|
130
|
+
/** Owner token banner; the configured default is named, never repeated; none for a foreign existing publication.
|
|
131
|
+
* Sign-in hint only for a token minted by this call (not `sent`, not the configured default). */
|
|
132
|
+
function ownerTokenLines(data, purpose, sent) {
|
|
133
|
+
const token = data.ownerToken;
|
|
134
|
+
const extras = data.result?.extras;
|
|
135
|
+
if (extras?.isOwner === false)
|
|
136
|
+
return [``, `No owner token: an existing publication of another owner was returned.`];
|
|
137
|
+
if (typeof token !== "string" || token.length === 0)
|
|
138
|
+
return [];
|
|
139
|
+
const shown = ENV_OWNER_TOKEN !== undefined && token === ENV_OWNER_TOKEN ? `${OWNER_TOKEN_ENV} (not repeated)` : token;
|
|
140
|
+
return [
|
|
141
|
+
``,
|
|
142
|
+
`===================================`,
|
|
143
|
+
`OWNER TOKEN: ${shown}`,
|
|
144
|
+
`===================================`,
|
|
145
|
+
``,
|
|
146
|
+
`IMPORTANT: Save your owner token! You need it to ${purpose}. It cannot be recovered if lost.`,
|
|
147
|
+
...(token !== sent && token !== ENV_OWNER_TOKEN ? [SIGN_IN_HINT] : []),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/** Private management link (present only when minted) and the public listing state. */
|
|
151
|
+
function ownerLinkLines(data) {
|
|
152
|
+
const lines = [];
|
|
153
|
+
if (typeof data.manageUrl === "string" && data.manageUrl.length > 0) {
|
|
154
|
+
lines.push(``, `MANAGE LINK (private): ${data.manageUrl}`, `Give it to the user privately; never publish it.`);
|
|
155
|
+
}
|
|
156
|
+
if (data.listed === true)
|
|
157
|
+
lines.push(``, `VISIBILITY: public, listed. Hide: set_visibility listed=false`);
|
|
158
|
+
return lines;
|
|
70
159
|
}
|
|
71
160
|
/** Optional delivery fields on HTML and hosting responses: why the mode was chosen, flagged or not applied. */
|
|
72
161
|
function deliveryReasonLines(data) {
|
|
@@ -80,7 +169,7 @@ function deliveryReasonLines(data) {
|
|
|
80
169
|
}
|
|
81
170
|
async function apiRequest(method, path, body, headers, responseType = "json") {
|
|
82
171
|
const url = `${BASE_URL}${path}`;
|
|
83
|
-
const res = await
|
|
172
|
+
const res = await bpFetch(url, {
|
|
84
173
|
method,
|
|
85
174
|
headers: {
|
|
86
175
|
"Content-Type": "application/json",
|
|
@@ -91,7 +180,7 @@ async function apiRequest(method, path, body, headers, responseType = "json") {
|
|
|
91
180
|
const data = responseType === "text" ? await res.text() : await res.json().catch(() => null);
|
|
92
181
|
return { ok: res.ok, status: res.status, data, headers: res.headers };
|
|
93
182
|
}
|
|
94
|
-
function formatPublishResponse(data, effectiveNamespace) {
|
|
183
|
+
function formatPublishResponse(data, effectiveNamespace, sentOwnerToken) {
|
|
95
184
|
const lines = [
|
|
96
185
|
`Published successfully!`,
|
|
97
186
|
``,
|
|
@@ -103,7 +192,8 @@ function formatPublishResponse(data, effectiveNamespace) {
|
|
|
103
192
|
if (data.expiresAt)
|
|
104
193
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
105
194
|
lines.push(...hostingResponseLines(data));
|
|
106
|
-
lines.push(
|
|
195
|
+
lines.push(...ownerTokenLines(data, "update or delete this resource later", sentOwnerToken));
|
|
196
|
+
lines.push(...ownerLinkLines(data));
|
|
107
197
|
const ns = (data.namespace ?? effectiveNamespace) || "";
|
|
108
198
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
109
199
|
lines.push(``, data.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
@@ -128,7 +218,8 @@ function formatUpdateResponse(data) {
|
|
|
128
218
|
}
|
|
129
219
|
server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repository default limit 5 MiB). Returns its primary share link and owner token. Supports password protection, custom TTL, optional filename, and an opt-in top toolbar (showTopBar). " +
|
|
130
220
|
"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; " +
|
|
131
|
-
"the result then includes deliveryModeReason and a delivery notice. Markdown raw HTML is allowlisted: scripts, styles, event handlers, forms and embeds are removed. Always share the returned URL."
|
|
221
|
+
"the result then includes deliveryModeReason and a delivery notice. Markdown raw HTML is allowlisted: scripts, styles, event handlers, forms and embeds are removed. Always share the returned URL. " +
|
|
222
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
132
223
|
content: z.string().describe("HTML or Markdown content to publish"),
|
|
133
224
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
134
225
|
format: z
|
|
@@ -154,7 +245,8 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repos
|
|
|
154
245
|
.boolean()
|
|
155
246
|
.optional()
|
|
156
247
|
.describe("Add a thin top toolbar (filename + Download button + theme toggle) on the served page. Default: hidden."),
|
|
157
|
-
|
|
248
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
249
|
+
}, async ({ content, format, namespace, password, ttlDays, filename, showTopBar, deliveryMode, ownerToken }) => {
|
|
158
250
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
159
251
|
const qsParams = [`ns=${encodeURIComponent(effectiveNamespace)}`, `format=${encodeURIComponent(format.toLowerCase())}`];
|
|
160
252
|
if (ttlDays !== undefined)
|
|
@@ -169,6 +261,9 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repos
|
|
|
169
261
|
headers["X-Password"] = password;
|
|
170
262
|
if (deliveryMode)
|
|
171
263
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
264
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
265
|
+
if (owner)
|
|
266
|
+
headers["X-Owner-Token"] = owner;
|
|
172
267
|
const { ok, data } = await apiRequest("POST", `/api/html?${qsParams.join("&")}`, body, headers);
|
|
173
268
|
if (!ok) {
|
|
174
269
|
return {
|
|
@@ -185,12 +280,13 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repos
|
|
|
185
280
|
content: [
|
|
186
281
|
{
|
|
187
282
|
type: "text",
|
|
188
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
283
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
189
284
|
},
|
|
190
285
|
],
|
|
191
286
|
};
|
|
192
287
|
});
|
|
193
|
-
server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defaults: 5 MiB for files/audio, 20 MiB for video. Returns its primary share link and owner token. Does not expose password or TTL inputs."
|
|
288
|
+
server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defaults: 5 MiB for files/audio, 20 MiB for video. Returns its primary share link and owner token. Does not expose password or TTL inputs. " +
|
|
289
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
194
290
|
url: z.string().url().describe("URL of the file to upload"),
|
|
195
291
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
196
292
|
namespace: z
|
|
@@ -198,7 +294,8 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defau
|
|
|
198
294
|
.optional()
|
|
199
295
|
.describe(NAMESPACE_DESCRIPTION),
|
|
200
296
|
filename: z.string().optional().describe("Custom filename (optional)"),
|
|
201
|
-
|
|
297
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
298
|
+
}, async ({ url: fileUrl, namespace, filename, deliveryMode, ownerToken }) => {
|
|
202
299
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
203
300
|
const fileRes = await fetch(fileUrl);
|
|
204
301
|
if (!fileRes.ok) {
|
|
@@ -215,9 +312,15 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defau
|
|
|
215
312
|
const blob = await fileRes.blob();
|
|
216
313
|
const formData = new FormData();
|
|
217
314
|
formData.append("file", blob, filename || fileUrl.split("/").pop() || "file");
|
|
218
|
-
const
|
|
315
|
+
const headers = {};
|
|
316
|
+
if (deliveryMode)
|
|
317
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
318
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
319
|
+
if (owner)
|
|
320
|
+
headers["X-Owner-Token"] = owner;
|
|
321
|
+
const res = await bpFetch(`${BASE_URL}/api/files?ns=${encodeURIComponent(effectiveNamespace)}`, {
|
|
219
322
|
method: "POST",
|
|
220
|
-
headers
|
|
323
|
+
headers,
|
|
221
324
|
body: formData,
|
|
222
325
|
});
|
|
223
326
|
const data = await res.json().catch(() => null);
|
|
@@ -236,7 +339,7 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defau
|
|
|
236
339
|
content: [
|
|
237
340
|
{
|
|
238
341
|
type: "text",
|
|
239
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
342
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
240
343
|
},
|
|
241
344
|
],
|
|
242
345
|
};
|
|
@@ -245,51 +348,36 @@ server.tool("update_hosting", "Change delivery mode for a NEW publication (not a
|
|
|
245
348
|
type: z.enum(["html", "file", "json", "kv"]).describe("Resource type; sites always stay on their own subdomain"),
|
|
246
349
|
namespace: z.string().describe("Resource namespace; non-public HTML-format pages stay on subdomain"),
|
|
247
350
|
id: z.string().describe("Resource ID"),
|
|
248
|
-
ownerToken: z.string().describe(
|
|
351
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
249
352
|
deliveryMode: z.enum(["path", "subdomain"]).describe("path = Promotion; subdomain = Dedicated subdomain"),
|
|
250
353
|
expectedVersion: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).optional().describe("Optional current hostingVersion from the server; a stale value still applies and the result says so"),
|
|
251
354
|
}, async ({ type, namespace, id, ownerToken, deliveryMode, expectedVersion }) => {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
}
|
|
355
|
+
const auth = ownerTokenFor(ownerToken);
|
|
356
|
+
if (!auth.token)
|
|
357
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
358
|
+
const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/hosting`;
|
|
359
|
+
const { ok, status, data } = await apiRequest("PATCH", path, { deliveryMode, expectedVersion }, { "X-Owner-Token": auth.token });
|
|
360
|
+
if (!ok)
|
|
361
|
+
return ownerFailure("update hosting", status, data, auth.fromEnv);
|
|
261
362
|
return {
|
|
262
363
|
content: [{ type: "text", text: formatUpdateResponse(data) }],
|
|
263
364
|
};
|
|
264
365
|
});
|
|
265
|
-
server.tool("delete_resource", "Delete a BrewPage resource (HTML page, KV store, JSON
|
|
366
|
+
server.tool("delete_resource", "Delete a BrewPage resource (HTML page, site, KV store, JSON document, or file) using the owner token.", {
|
|
266
367
|
type: z
|
|
267
|
-
.enum(["html", "kv", "json", "file"])
|
|
368
|
+
.enum(["html", "site", "kv", "json", "file"])
|
|
268
369
|
.describe("Resource type to delete"),
|
|
269
370
|
namespace: z.string().describe("Resource namespace"),
|
|
270
371
|
id: z.string().describe("Resource ID"),
|
|
271
|
-
ownerToken: z
|
|
272
|
-
.string()
|
|
273
|
-
.describe("Owner token received when the resource was created"),
|
|
372
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
274
373
|
}, async ({ type, namespace, id, ownerToken }) => {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (!ok) {
|
|
283
|
-
return {
|
|
284
|
-
content: [
|
|
285
|
-
{
|
|
286
|
-
type: "text",
|
|
287
|
-
text: `Failed to delete (${status}): ${JSON.stringify(data)}`,
|
|
288
|
-
},
|
|
289
|
-
],
|
|
290
|
-
isError: true,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
374
|
+
const auth = ownerTokenFor(ownerToken);
|
|
375
|
+
if (!auth.token)
|
|
376
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
377
|
+
const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
|
|
378
|
+
const { ok, status, data } = await apiRequest("DELETE", path, undefined, { "X-Owner-Token": auth.token });
|
|
379
|
+
if (!ok)
|
|
380
|
+
return ownerFailure("delete", status, data, auth.fromEnv);
|
|
293
381
|
return {
|
|
294
382
|
content: [
|
|
295
383
|
{
|
|
@@ -372,7 +460,8 @@ server.tool("get_stats", "Get BrewPage platform-wide usage statistics including
|
|
|
372
460
|
],
|
|
373
461
|
};
|
|
374
462
|
});
|
|
375
|
-
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`. Use its returned primary link and retain the owner token. " + SITE_LIMITS_TEXT
|
|
463
|
+
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`. Use its returned primary link and retain the owner token. " + SITE_LIMITS_TEXT +
|
|
464
|
+
" " + MANAGE_URL_TEXT + " " + SECRETS, {
|
|
376
465
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
377
466
|
entryContent: z
|
|
378
467
|
.string()
|
|
@@ -395,10 +484,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
395
484
|
.optional()
|
|
396
485
|
.describe("Optional password to protect the site"),
|
|
397
486
|
ttlDays: TTL_SCHEMA,
|
|
398
|
-
ownerToken: z
|
|
399
|
-
.string()
|
|
400
|
-
.optional()
|
|
401
|
-
.describe("Existing owner token to group this site under the same owner as previous content"),
|
|
487
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
402
488
|
}, async ({ entryContent, files, entry, namespace, password, ttlDays, ownerToken, deliveryMode }) => {
|
|
403
489
|
const hasEntry = typeof entryContent === "string";
|
|
404
490
|
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
@@ -429,8 +515,9 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
429
515
|
const headers = {};
|
|
430
516
|
if (password)
|
|
431
517
|
headers["X-Password"] = password;
|
|
432
|
-
|
|
433
|
-
|
|
518
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
519
|
+
if (owner)
|
|
520
|
+
headers["X-Owner-Token"] = owner;
|
|
434
521
|
if (deliveryMode)
|
|
435
522
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
436
523
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
@@ -442,7 +529,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
442
529
|
qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
|
|
443
530
|
const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
|
|
444
531
|
const url = `${BASE_URL}/api/sites${qs}`;
|
|
445
|
-
const res = await
|
|
532
|
+
const res = await bpFetch(url, { method: "POST", headers, body: formData });
|
|
446
533
|
const data = await res.json().catch(() => null);
|
|
447
534
|
if (!res.ok) {
|
|
448
535
|
return {
|
|
@@ -474,7 +561,8 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
474
561
|
if (site.expiresAt)
|
|
475
562
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
476
563
|
lines.push(...hostingResponseLines(site));
|
|
477
|
-
lines.push(
|
|
564
|
+
lines.push(...ownerTokenLines(site, "view info or delete this site", owner));
|
|
565
|
+
lines.push(...ownerLinkLines(site));
|
|
478
566
|
const ns = (site.namespace || effectiveNamespace) || "";
|
|
479
567
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
480
568
|
lines.push(``, site.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
@@ -483,12 +571,10 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
483
571
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
484
572
|
};
|
|
485
573
|
});
|
|
486
|
-
server.tool("republish_site", "Republish (full-replace) an existing multi-file BrewPage site in place at the SAME URL/ID. Requires owner token. The uploaded bundle becomes the complete new file set — files absent from it are removed, new files added, matching files overwritten. Single-page: pass `entryContent`. Multi-page: pass `files` (array of `{path, content}`). Optional `entry` overrides the default `index.html`. Republishing preserves existing expiry. " + SITE_LIMITS_TEXT, {
|
|
574
|
+
server.tool("republish_site", "Republish (full-replace) an existing multi-file BrewPage site in place at the SAME URL/ID. Requires owner token. The uploaded bundle becomes the complete new file set — files absent from it are removed, new files added, matching files overwritten. Single-page: pass `entryContent`. Multi-page: pass `files` (array of `{path, content}`). Optional `entry` overrides the default `index.html`. Republishing preserves existing expiry." + OLD_READ_ONLY_TEXT + " " + SITE_LIMITS_TEXT, {
|
|
487
575
|
namespace: z.string().describe("Site namespace"),
|
|
488
576
|
id: z.string().describe("Site ID"),
|
|
489
|
-
ownerToken: z
|
|
490
|
-
.string()
|
|
491
|
-
.describe("Owner token received when the site was created (required for republish)"),
|
|
577
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
492
578
|
entryContent: z
|
|
493
579
|
.string()
|
|
494
580
|
.optional()
|
|
@@ -533,7 +619,10 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
533
619
|
formData.append("files", entryBlob, entryName);
|
|
534
620
|
formData.append("paths", entryName);
|
|
535
621
|
}
|
|
536
|
-
const
|
|
622
|
+
const auth = ownerTokenFor(ownerToken);
|
|
623
|
+
if (!auth.token)
|
|
624
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
625
|
+
const headers = { "X-Owner-Token": auth.token };
|
|
537
626
|
const qsParams = [];
|
|
538
627
|
if (entry)
|
|
539
628
|
qsParams.push(`entry=${encodeURIComponent(entry)}`);
|
|
@@ -543,19 +632,10 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
543
632
|
qsParams.push(`tags=${encodeURIComponent(tags.join(","))}`);
|
|
544
633
|
const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
|
|
545
634
|
const url = `${BASE_URL}/api/sites/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}${qs}`;
|
|
546
|
-
const res = await
|
|
635
|
+
const res = await bpFetch(url, { method: "PUT", headers, body: formData });
|
|
547
636
|
const data = await res.json().catch(() => null);
|
|
548
|
-
if (!res.ok)
|
|
549
|
-
return
|
|
550
|
-
content: [
|
|
551
|
-
{
|
|
552
|
-
type: "text",
|
|
553
|
-
text: `Failed to republish site (${res.status}): ${JSON.stringify(data)}`,
|
|
554
|
-
},
|
|
555
|
-
],
|
|
556
|
-
isError: true,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
637
|
+
if (!res.ok)
|
|
638
|
+
return ownerFailure("republish site", res.status, data, auth.fromEnv);
|
|
559
639
|
const site = data;
|
|
560
640
|
const lines = [
|
|
561
641
|
`Site republished successfully (full replace)!`,
|
|
@@ -584,13 +664,11 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
584
664
|
};
|
|
585
665
|
});
|
|
586
666
|
server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requires owner token. Returns the updated page metadata. " +
|
|
587
|
-
"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.", {
|
|
667
|
+
"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." + OLD_READ_ONLY_TEXT, {
|
|
588
668
|
namespace: z.string().describe("Page namespace"),
|
|
589
669
|
id: z.string().describe("Page ID"),
|
|
590
670
|
content: z.string().describe("New HTML or Markdown content"),
|
|
591
|
-
ownerToken: z
|
|
592
|
-
.string()
|
|
593
|
-
.describe("Owner token received when the page was created"),
|
|
671
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
594
672
|
format: z
|
|
595
673
|
.string()
|
|
596
674
|
.optional()
|
|
@@ -599,21 +677,15 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
599
677
|
const body = { content };
|
|
600
678
|
if (format)
|
|
601
679
|
body.format = format;
|
|
680
|
+
const auth = ownerTokenFor(ownerToken);
|
|
681
|
+
if (!auth.token)
|
|
682
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
602
683
|
const path = `/api/html/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
|
|
603
684
|
const { ok, status, data } = await apiRequest("PUT", path, body, {
|
|
604
|
-
"X-Owner-Token":
|
|
685
|
+
"X-Owner-Token": auth.token,
|
|
605
686
|
});
|
|
606
|
-
if (!ok)
|
|
607
|
-
return
|
|
608
|
-
content: [
|
|
609
|
-
{
|
|
610
|
-
type: "text",
|
|
611
|
-
text: `Failed to update HTML (${status}): ${JSON.stringify(data)}`,
|
|
612
|
-
},
|
|
613
|
-
],
|
|
614
|
-
isError: true,
|
|
615
|
-
};
|
|
616
|
-
}
|
|
687
|
+
if (!ok)
|
|
688
|
+
return ownerFailure("update HTML", status, data, auth.fromEnv);
|
|
617
689
|
return {
|
|
618
690
|
content: [
|
|
619
691
|
{
|
|
@@ -623,7 +695,8 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
623
695
|
],
|
|
624
696
|
};
|
|
625
697
|
});
|
|
626
|
-
server.tool("publish_json", "Publish a JSON document to BrewPage (repository default limit 1 MiB). Accepts a JSON object, array, or a JSON-encoded string containing any valid JSON, including scalars/null. Returns its primary share link and owner token."
|
|
698
|
+
server.tool("publish_json", "Publish a JSON document to BrewPage (repository default limit 1 MiB). Accepts a JSON object, array, or a JSON-encoded string containing any valid JSON, including scalars/null. Returns its primary share link and owner token. " +
|
|
699
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
627
700
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
628
701
|
json: z
|
|
629
702
|
.union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
|
|
@@ -641,10 +714,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage (repository def
|
|
|
641
714
|
.array(z.string())
|
|
642
715
|
.optional()
|
|
643
716
|
.describe("Optional tags for the document"),
|
|
644
|
-
ownerToken: z
|
|
645
|
-
.string()
|
|
646
|
-
.optional()
|
|
647
|
-
.describe("Existing owner token to group this document under the same owner as previous content"),
|
|
717
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
648
718
|
}, async ({ json, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
649
719
|
const jsonString = typeof json === "string" ? json : JSON.stringify(json);
|
|
650
720
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
@@ -660,11 +730,12 @@ server.tool("publish_json", "Publish a JSON document to BrewPage (repository def
|
|
|
660
730
|
const headers = { "Content-Type": "application/json" };
|
|
661
731
|
if (password)
|
|
662
732
|
headers["X-Password"] = password;
|
|
663
|
-
|
|
664
|
-
|
|
733
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
734
|
+
if (owner)
|
|
735
|
+
headers["X-Owner-Token"] = owner;
|
|
665
736
|
if (deliveryMode)
|
|
666
737
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
667
|
-
const res = await
|
|
738
|
+
const res = await bpFetch(`${BASE_URL}/api/json${qs}`, {
|
|
668
739
|
method: "POST",
|
|
669
740
|
headers,
|
|
670
741
|
body: jsonString,
|
|
@@ -685,7 +756,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage (repository def
|
|
|
685
756
|
content: [
|
|
686
757
|
{
|
|
687
758
|
type: "text",
|
|
688
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
759
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
689
760
|
},
|
|
690
761
|
],
|
|
691
762
|
};
|
|
@@ -723,38 +794,30 @@ server.tool("get_json", "Fetch a published BrewPage JSON document by namespace a
|
|
|
723
794
|
],
|
|
724
795
|
};
|
|
725
796
|
});
|
|
726
|
-
server.tool("update_json", "Update an existing BrewPage JSON document. Requires owner token. Accepts a JSON object, array, or a JSON-encoded string.", {
|
|
797
|
+
server.tool("update_json", "Update an existing BrewPage JSON document. Requires owner token. Accepts a JSON object, array, or a JSON-encoded string." + OLD_READ_ONLY_TEXT, {
|
|
727
798
|
namespace: z.string().describe("Document namespace"),
|
|
728
799
|
id: z.string().describe("Document ID"),
|
|
729
800
|
json: z
|
|
730
801
|
.union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
|
|
731
802
|
.describe("New JSON document (object, array, or JSON-encoded string)"),
|
|
732
|
-
ownerToken: z
|
|
733
|
-
.string()
|
|
734
|
-
.describe("Owner token received when the document was created"),
|
|
803
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
735
804
|
}, async ({ namespace, id, json, ownerToken }) => {
|
|
736
805
|
const jsonString = typeof json === "string" ? json : JSON.stringify(json);
|
|
806
|
+
const auth = ownerTokenFor(ownerToken);
|
|
807
|
+
if (!auth.token)
|
|
808
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
737
809
|
const url = `${BASE_URL}/api/json/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
|
|
738
|
-
const res = await
|
|
810
|
+
const res = await bpFetch(url, {
|
|
739
811
|
method: "PUT",
|
|
740
812
|
headers: {
|
|
741
813
|
"Content-Type": "application/json",
|
|
742
|
-
"X-Owner-Token":
|
|
814
|
+
"X-Owner-Token": auth.token,
|
|
743
815
|
},
|
|
744
816
|
body: jsonString,
|
|
745
817
|
});
|
|
746
818
|
const data = await res.json().catch(() => null);
|
|
747
|
-
if (!res.ok)
|
|
748
|
-
return
|
|
749
|
-
content: [
|
|
750
|
-
{
|
|
751
|
-
type: "text",
|
|
752
|
-
text: `Failed to update JSON (${res.status}): ${JSON.stringify(data)}`,
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
isError: true,
|
|
756
|
-
};
|
|
757
|
-
}
|
|
819
|
+
if (!res.ok)
|
|
820
|
+
return ownerFailure("update JSON", res.status, data, auth.fromEnv);
|
|
758
821
|
return {
|
|
759
822
|
content: [
|
|
760
823
|
{
|
|
@@ -764,7 +827,8 @@ server.tool("update_json", "Update an existing BrewPage JSON document. Requires
|
|
|
764
827
|
],
|
|
765
828
|
};
|
|
766
829
|
});
|
|
767
|
-
server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV store; the entry is addressed by (namespace, id, key). Repository defaults: 1 MiB per value, 1,000 keys per store. Returns its primary share link and owner token."
|
|
830
|
+
server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV store; the entry is addressed by (namespace, id, key). Repository defaults: 1 MiB per value, 1,000 keys per store; the key name `hosting` is reserved. Returns its primary share link and owner token. " +
|
|
831
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
768
832
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
769
833
|
key: z.string().describe("KV key (first part of the (namespace, id, key) addressing tuple)"),
|
|
770
834
|
value: z.string().describe("KV value"),
|
|
@@ -781,10 +845,7 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
781
845
|
.array(z.string())
|
|
782
846
|
.optional()
|
|
783
847
|
.describe("Optional tags for the KV entry"),
|
|
784
|
-
ownerToken: z
|
|
785
|
-
.string()
|
|
786
|
-
.optional()
|
|
787
|
-
.describe("Existing owner token to group this KV store under the same owner as previous content"),
|
|
848
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
788
849
|
}, async ({ key, value, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
789
850
|
const body = { key, value };
|
|
790
851
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
@@ -800,8 +861,9 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
800
861
|
const headers = {};
|
|
801
862
|
if (password)
|
|
802
863
|
headers["X-Password"] = password;
|
|
803
|
-
|
|
804
|
-
|
|
864
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
865
|
+
if (owner)
|
|
866
|
+
headers["X-Owner-Token"] = owner;
|
|
805
867
|
if (deliveryMode)
|
|
806
868
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
807
869
|
const { ok, status, data } = await apiRequest("POST", `/api/kv${qs}`, body, headers);
|
|
@@ -820,36 +882,28 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
820
882
|
content: [
|
|
821
883
|
{
|
|
822
884
|
type: "text",
|
|
823
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
885
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
824
886
|
},
|
|
825
887
|
],
|
|
826
888
|
};
|
|
827
889
|
});
|
|
828
|
-
server.tool("set_kv", "Set or update a key in an existing BrewPage KV store. Requires owner token.", {
|
|
890
|
+
server.tool("set_kv", "Set or update a key in an existing BrewPage KV store. Requires owner token. The key name `hosting` is reserved." + OLD_READ_ONLY_TEXT, {
|
|
829
891
|
namespace: z.string().describe("KV store namespace"),
|
|
830
892
|
id: z.string().describe("KV store ID"),
|
|
831
893
|
key: z.string().describe("Key to set"),
|
|
832
894
|
value: z.string().describe("Value to assign to the key"),
|
|
833
|
-
ownerToken: z
|
|
834
|
-
.string()
|
|
835
|
-
.describe("Owner token received when the KV store was created"),
|
|
895
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
836
896
|
}, async ({ namespace, id, key, value, ownerToken }) => {
|
|
837
897
|
const body = { value };
|
|
898
|
+
const auth = ownerTokenFor(ownerToken);
|
|
899
|
+
if (!auth.token)
|
|
900
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
838
901
|
const path = `/api/kv/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/${encodeURIComponent(key)}`;
|
|
839
902
|
const { ok, status, data } = await apiRequest("PUT", path, body, {
|
|
840
|
-
"X-Owner-Token":
|
|
903
|
+
"X-Owner-Token": auth.token,
|
|
841
904
|
});
|
|
842
|
-
if (!ok)
|
|
843
|
-
return
|
|
844
|
-
content: [
|
|
845
|
-
{
|
|
846
|
-
type: "text",
|
|
847
|
-
text: `Failed to set KV (${status}): ${JSON.stringify(data)}`,
|
|
848
|
-
},
|
|
849
|
-
],
|
|
850
|
-
isError: true,
|
|
851
|
-
};
|
|
852
|
-
}
|
|
905
|
+
if (!ok)
|
|
906
|
+
return ownerFailure("set KV", status, data, auth.fromEnv);
|
|
853
907
|
return {
|
|
854
908
|
content: [
|
|
855
909
|
{
|
|
@@ -893,10 +947,11 @@ server.tool("get_kv", "Fetch a value from a BrewPage KV store by namespace, ID,
|
|
|
893
947
|
],
|
|
894
948
|
};
|
|
895
949
|
});
|
|
896
|
-
server.tool("get_limits", "Get the current BrewPage upload limits (GET /api/limits
|
|
950
|
+
server.tool("get_limits", "Get the current BrewPage upload limits (GET /api/limits): request size, site file count, per-file and total size, " +
|
|
897
951
|
"ZIP expansion, path and name lengths, TTL default/maximum, allowed file extensions and skipped system files. Sizes are bytes. " +
|
|
898
|
-
|
|
899
|
-
const
|
|
952
|
+
`With ${OWNER_TOKEN_ENV} set: also reach (token|profile) and ownerMaxTtlDays. Call it before publishing a large or multi-file site.`, {}, async () => {
|
|
953
|
+
const headers = ENV_OWNER_TOKEN ? { "X-Owner-Token": ENV_OWNER_TOKEN } : {};
|
|
954
|
+
const { ok, status, data } = await apiRequest("GET", "/api/limits", undefined, headers);
|
|
900
955
|
if (!ok) {
|
|
901
956
|
return {
|
|
902
957
|
content: [
|
|
@@ -943,8 +998,11 @@ server.tool("search_gallery", "Search the BrewPage public gallery. Optional filt
|
|
|
943
998
|
ownerToken: z
|
|
944
999
|
.string()
|
|
945
1000
|
.optional()
|
|
946
|
-
.describe(
|
|
1001
|
+
.describe(`Owner token; only sent as X-Owner-Token when 'mine' is true. Default: ${OWNER_TOKEN_ENV}.`),
|
|
947
1002
|
}, async ({ q, page, size, sort, mine, ownerToken }) => {
|
|
1003
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1004
|
+
if (mine === true && !auth.token)
|
|
1005
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
948
1006
|
const qsParams = [];
|
|
949
1007
|
if (q !== undefined)
|
|
950
1008
|
qsParams.push(`q=${encodeURIComponent(q)}`);
|
|
@@ -958,8 +1016,8 @@ server.tool("search_gallery", "Search the BrewPage public gallery. Optional filt
|
|
|
958
1016
|
qsParams.push("mine=true");
|
|
959
1017
|
const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
|
|
960
1018
|
const headers = {};
|
|
961
|
-
if (mine === true &&
|
|
962
|
-
headers["X-Owner-Token"] =
|
|
1019
|
+
if (mine === true && auth.token)
|
|
1020
|
+
headers["X-Owner-Token"] = auth.token;
|
|
963
1021
|
const { ok, status, data } = await apiRequest("GET", `/api/gallery${qs}`, undefined, headers);
|
|
964
1022
|
if (!ok) {
|
|
965
1023
|
return {
|
|
@@ -981,6 +1039,59 @@ server.tool("search_gallery", "Search the BrewPage public gallery. Optional filt
|
|
|
981
1039
|
],
|
|
982
1040
|
};
|
|
983
1041
|
});
|
|
1042
|
+
server.tool("set_visibility", "Hide or show a public publication in gallery & search. Link unchanged.", {
|
|
1043
|
+
type: z.enum(["html", "site", "file", "json", "kv"]).describe("Publication type; only public html, site and file can be listed"),
|
|
1044
|
+
namespace: z.string().describe("Publication namespace"),
|
|
1045
|
+
id: z.string().describe("Publication ID"),
|
|
1046
|
+
listed: z.boolean().describe("false = hide, true = show"),
|
|
1047
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
1048
|
+
}, async ({ type, namespace, id, listed, ownerToken }) => {
|
|
1049
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1050
|
+
if (!auth.token)
|
|
1051
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
1052
|
+
const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/visibility`;
|
|
1053
|
+
const { ok, status, data } = await apiRequest("POST", path, { listed }, { "X-Owner-Token": auth.token });
|
|
1054
|
+
if (!ok)
|
|
1055
|
+
return ownerFailure("set visibility", status, data, auth.fromEnv);
|
|
1056
|
+
const result = data;
|
|
1057
|
+
return textResult([`LISTED: ${result.listed} · link unchanged`, ...noticeLines(result)].join("\n"));
|
|
1058
|
+
});
|
|
1059
|
+
server.tool("bind_token", "Add an owner token to your BrewPage profile (as publish). Needs an admin token of that profile.", {
|
|
1060
|
+
token: z.string().describe("Owner token to add"),
|
|
1061
|
+
ownerToken: z.string().optional().describe(`Admin token of the profile. Default: ${OWNER_TOKEN_ENV}.`),
|
|
1062
|
+
}, async ({ token, ownerToken }) => {
|
|
1063
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1064
|
+
if (!auth.token)
|
|
1065
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
1066
|
+
const { ok, status, data } = await apiRequest("POST", "/api/owner-token/bind", { token }, { "X-Owner-Token": auth.token });
|
|
1067
|
+
if (!ok)
|
|
1068
|
+
return textResult(`Failed to bind token (${status}): ${JSON.stringify(data)}`, true);
|
|
1069
|
+
const result = data;
|
|
1070
|
+
return textResult(`BOUND ${maskToken(token)} · ${result.active}/${result.max} active`);
|
|
1071
|
+
});
|
|
1072
|
+
server.tool("mint_token", "Mint a new profile token with an admin token; the new token is shown once. Scope publish (default) or admin; expiry 30d, 90d, 1y or never.", {
|
|
1073
|
+
label: z.string().optional().describe("Optional label shown in the profile"),
|
|
1074
|
+
scope: z.enum(["publish", "admin"]).optional().describe("publish (default) or admin (can manage tokens)"),
|
|
1075
|
+
expiresIn: z.enum(["30d", "90d", "1y", "never"]).optional().describe("Default: admin 90d, publish never"),
|
|
1076
|
+
ownerToken: z.string().optional().describe(`Admin token of the profile. Default: ${OWNER_TOKEN_ENV}.`),
|
|
1077
|
+
}, async ({ label, scope, expiresIn, ownerToken }) => {
|
|
1078
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1079
|
+
if (!auth.token)
|
|
1080
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
1081
|
+
const { ok, status, data } = await apiRequest("POST", "/api/owner-token/mint", { label, scope, expiresIn }, { "X-Owner-Token": auth.token });
|
|
1082
|
+
if (!ok)
|
|
1083
|
+
return textResult(`Failed to mint token (${status}): ${JSON.stringify(data)}`, true);
|
|
1084
|
+
const result = data;
|
|
1085
|
+
return textResult([
|
|
1086
|
+
`MINTED · ${result.scope} · expires ${result.expiresAt ?? "never"}`,
|
|
1087
|
+
``,
|
|
1088
|
+
`===================================`,
|
|
1089
|
+
`NEW TOKEN (shown once): ${result.token}`,
|
|
1090
|
+
`===================================`,
|
|
1091
|
+
``,
|
|
1092
|
+
`Save it now; it cannot be shown again. Never publish it.`,
|
|
1093
|
+
].join("\n"));
|
|
1094
|
+
});
|
|
984
1095
|
async function main() {
|
|
985
1096
|
const transport = new StdioServerTransport();
|
|
986
1097
|
await server.connect(transport);
|
package/package.json
CHANGED