brewpage-mcp 1.12.0 → 1.13.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 +67 -17
- package/dist/index.js +248 -145
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +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.
|
|
14
|
+
|
|
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).
|
|
11
16
|
|
|
12
17
|
## Quick Start
|
|
13
18
|
|
|
14
19
|
```bash
|
|
15
|
-
npx -y brewpage-mcp@1.
|
|
20
|
+
npx -y brewpage-mcp@1.13.0
|
|
16
21
|
```
|
|
17
22
|
|
|
18
23
|
Requires Node.js 20 or newer. This starts the stdio server for an MCP client.
|
|
@@ -28,7 +33,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
28
33
|
"mcpServers": {
|
|
29
34
|
"brewpage": {
|
|
30
35
|
"command": "npx",
|
|
31
|
-
"args": ["-y", "brewpage-mcp@1.
|
|
36
|
+
"args": ["-y", "brewpage-mcp@1.13.0"]
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
}
|
|
@@ -39,7 +44,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
39
44
|
Register a project-scoped stdio server:
|
|
40
45
|
|
|
41
46
|
```bash
|
|
42
|
-
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.0
|
|
43
48
|
```
|
|
44
49
|
|
|
45
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).
|
|
@@ -53,7 +58,7 @@ Open **Settings > MCP** and add:
|
|
|
53
58
|
"mcpServers": {
|
|
54
59
|
"brewpage": {
|
|
55
60
|
"command": "npx",
|
|
56
|
-
"args": ["-y", "brewpage-mcp@1.
|
|
61
|
+
"args": ["-y", "brewpage-mcp@1.13.0"]
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -67,7 +72,7 @@ Open the Cline MCP settings panel and add:
|
|
|
67
72
|
{
|
|
68
73
|
"brewpage": {
|
|
69
74
|
"command": "npx",
|
|
70
|
-
"args": ["-y", "brewpage-mcp@1.
|
|
75
|
+
"args": ["-y", "brewpage-mcp@1.13.0"]
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
```
|
|
@@ -75,13 +80,13 @@ Open the Cline MCP settings panel and add:
|
|
|
75
80
|
### Global Install
|
|
76
81
|
|
|
77
82
|
```bash
|
|
78
|
-
npm install -g brewpage-mcp@1.
|
|
83
|
+
npm install -g brewpage-mcp@1.13.0
|
|
79
84
|
brewpage-mcp
|
|
80
85
|
```
|
|
81
86
|
|
|
82
87
|
## Tools
|
|
83
88
|
|
|
84
|
-
|
|
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.
|
|
85
90
|
|
|
86
91
|
| Tool | Purpose |
|
|
87
92
|
|------|---------|
|
|
@@ -98,14 +103,17 @@ Seventeen tools are available. Creation returns an owner token; keep it to modif
|
|
|
98
103
|
| `set_kv` | Set or replace a key in an owned KV store |
|
|
99
104
|
| `get_kv` | Read a KV value |
|
|
100
105
|
| `update_hosting` | Change eligible delivery mode with owner token and hosting version |
|
|
101
|
-
| `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) |
|
|
102
110
|
| `search_gallery` | Browse public content or list owned resources |
|
|
103
111
|
| `get_stats` | Read platform-wide statistics |
|
|
104
112
|
| `get_limits` | Read the current upload limits (`GET /api/limits`) |
|
|
105
113
|
|
|
106
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.
|
|
107
115
|
|
|
108
|
-
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.
|
|
109
117
|
|
|
110
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.
|
|
111
119
|
|
|
@@ -339,7 +347,7 @@ get_kv(namespace="public", id="aBcDeFgHiJ", key="theme")
|
|
|
339
347
|
|
|
340
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.
|
|
341
349
|
|
|
342
|
-
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`).
|
|
343
351
|
|
|
344
352
|
Example prompt:
|
|
345
353
|
|
|
@@ -355,11 +363,9 @@ search_gallery(mine=true, ownerToken="tok_...", sort="date")
|
|
|
355
363
|
|
|
356
364
|
### `delete_resource`
|
|
357
365
|
|
|
358
|
-
Delete a BrewPage resource (HTML page, KV store, JSON document, or file) using the owner token received at creation.
|
|
366
|
+
Delete a BrewPage resource (HTML page, site, KV store, JSON document, or file) using the owner token received at creation.
|
|
359
367
|
|
|
360
|
-
Parameters: `type` (`html` | `kv` | `json` | `file`), `namespace` (string), `id` (string), `ownerToken` (
|
|
361
|
-
|
|
362
|
-
Sites are not supported by this tool; use the REST site deletion endpoint.
|
|
368
|
+
Parameters: `type` (`html` | `site` | `kv` | `json` | `file`), `namespace` (string), `id` (string), `ownerToken` (optional; default `BREWPAGE_OWNER_TOKEN`). Path segments are URL-encoded.
|
|
363
369
|
|
|
364
370
|
Example prompt:
|
|
365
371
|
|
|
@@ -371,6 +377,36 @@ delete_resource(type="html", namespace="public", id="aBcDeFgHiJ", ownerToken="to
|
|
|
371
377
|
|
|
372
378
|
---
|
|
373
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
|
+
|
|
374
410
|
### `get_stats`
|
|
375
411
|
|
|
376
412
|
Get platform-wide BrewPage usage statistics (page count, file count, storage, daily totals). Supports an optional IANA timezone for the "today" boundary.
|
|
@@ -389,7 +425,7 @@ get_stats()
|
|
|
389
425
|
|
|
390
426
|
### `get_limits`
|
|
391
427
|
|
|
392
|
-
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.
|
|
393
429
|
|
|
394
430
|
Parameters: none.
|
|
395
431
|
|
|
@@ -430,7 +466,8 @@ get_limits()
|
|
|
430
466
|
|
|
431
467
|
Every publish response includes an **owner token** -- the only credential that allows updating or deleting your content. **Save it. It cannot be recovered.**
|
|
432
468
|
|
|
433
|
-
- 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.
|
|
434
471
|
- Pass it to `delete_resource` to remove content.
|
|
435
472
|
- Pass it to `search_gallery` with `mine=true` to list owned resources. `get_page` has no owner-token input; protected reads use `password`.
|
|
436
473
|
|
|
@@ -439,6 +476,7 @@ Every publish response includes an **owner token** -- the only credential that a
|
|
|
439
476
|
| Variable | Default | Description |
|
|
440
477
|
|----------|---------|-------------|
|
|
441
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 |
|
|
442
480
|
|
|
443
481
|
## Links
|
|
444
482
|
|
|
@@ -453,6 +491,18 @@ Every publish response includes an **owner token** -- the only credential that a
|
|
|
453
491
|
|
|
454
492
|
## Changelog
|
|
455
493
|
|
|
494
|
+
## 1.13.0 -- 2026-10-25
|
|
495
|
+
|
|
496
|
+
- Identifying `User-Agent: brewpage-mcp/1.13.0` on every BrewPage call.
|
|
497
|
+
- Optional `BREWPAGE_OWNER_TOKEN` default; `ownerToken` on `publish_html` and `publish_file`; owner tools fail before HTTP without a token.
|
|
498
|
+
- New tools: `set_visibility`, `bind_token`, `mint_token` (twenty tools).
|
|
499
|
+
- `delete_resource` deletes sites and URL-encodes path segments.
|
|
500
|
+
- Prints `MANAGE LINK (private)`, `VISIBILITY` and `WARNING (code)` lines; a wrong-token 403 adds a hint.
|
|
501
|
+
|
|
502
|
+
## 1.12.1 -- 2026-10-22
|
|
503
|
+
|
|
504
|
+
- Package README links the BrewPage comparison page (https://brewpage.app/compare). Same seventeen tools, inputs and HTTP behavior.
|
|
505
|
+
|
|
456
506
|
## 1.12.0 -- 2026-10-22
|
|
457
507
|
|
|
458
508
|
- New `get_limits` tool reads `GET /api/limits`: request size, site file count, per-file and total size, ZIP expansion, path and name lengths, TTL default/maximum, allowed extensions and skipped system files.
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,60 @@ 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.0";
|
|
6
7
|
const server = new McpServer({
|
|
7
8
|
name: "brewpage-mcp",
|
|
8
|
-
version: "1.
|
|
9
|
+
version: "1.13.0",
|
|
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
|
+
/** Notice codes the agent must relay to the user. */
|
|
29
|
+
const WARNING_CODES = new Set(["secret-detected", "manage-link-disabled"]);
|
|
30
|
+
/** MCP type name -> REST path segment. */
|
|
31
|
+
const TYPE_SEGMENTS = { html: "html", site: "sites", file: "files", json: "json", kv: "kv" };
|
|
32
|
+
/** Caller argument first, then the configured default; `fromEnv` marks the configured one. */
|
|
33
|
+
function ownerTokenFor(argument) {
|
|
34
|
+
if (argument && argument.length > 0)
|
|
35
|
+
return { token: argument, fromEnv: false };
|
|
36
|
+
return { token: ENV_OWNER_TOKEN, fromEnv: ENV_OWNER_TOKEN !== undefined };
|
|
37
|
+
}
|
|
38
|
+
/** Only the last 4 characters: tokens never reach a transcript in full. */
|
|
39
|
+
function maskToken(token) {
|
|
40
|
+
return `…${token.slice(-4)}`;
|
|
41
|
+
}
|
|
42
|
+
function textResult(text, isError = false) {
|
|
43
|
+
return isError ? { content: [{ type: "text", text }], isError: true } : { content: [{ type: "text", text }] };
|
|
44
|
+
}
|
|
45
|
+
/** Owner-tool failure: the server answer verbatim plus a hint for a plain wrong-token 403. */
|
|
46
|
+
function ownerFailure(operation, status, data, fromEnv) {
|
|
47
|
+
const lines = [`Failed to ${operation} (${status}): ${JSON.stringify(data)}`];
|
|
48
|
+
const code = data?.code;
|
|
49
|
+
if (status === 403 && (code === undefined || code === "OWNER_AUTHORIZATION_FAILED")) {
|
|
50
|
+
lines.push(`Wrong owner token for this publication. Use ownerToken from create.${fromEnv ? ` Sent: ${OWNER_TOKEN_ENV}.` : ""}`);
|
|
51
|
+
}
|
|
52
|
+
return textResult(lines.join("\n"), true);
|
|
53
|
+
}
|
|
54
|
+
/** Every BrewPage call identifies the client; source-file downloads use plain fetch. */
|
|
55
|
+
function bpFetch(url, init = {}) {
|
|
56
|
+
const headers = new Headers(init.headers);
|
|
57
|
+
headers.set("User-Agent", USER_AGENT);
|
|
58
|
+
return fetch(url, { ...init, headers });
|
|
59
|
+
}
|
|
10
60
|
const PUBLIC_NAMESPACE = "public";
|
|
11
61
|
/**
|
|
12
62
|
* Generate a short, unlisted private namespace slug used when the caller omits
|
|
@@ -29,13 +79,13 @@ function defaultPrivateNamespace() {
|
|
|
29
79
|
function resolveNamespace(namespace) {
|
|
30
80
|
return namespace && namespace.length > 0 ? namespace : defaultPrivateNamespace();
|
|
31
81
|
}
|
|
32
|
-
const UNLISTED_NOTICE = "
|
|
33
|
-
|
|
34
|
-
const PROTECTED_UNLISTED_NOTICE = "Password-protected unlisted link — readers need the password; excluded from gallery and search.";
|
|
82
|
+
const UNLISTED_NOTICE = "Private: not listed. Anyone with the link can open it. Use namespace public for gallery & search.";
|
|
83
|
+
const PROTECTED_UNLISTED_NOTICE = "Password: link + password. Not listed.";
|
|
35
84
|
const NAMESPACE_DESCRIPTION = "Optional. Omit to generate an unlisted namespace: anyone with its unpassworded link can read it, " +
|
|
36
85
|
"but it is excluded from gallery/search. Pass a custom namespace to group unlisted content, " +
|
|
37
86
|
"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."
|
|
87
|
+
"Password-protected content stays excluded; unlisted, protected or otherwise ineligible native responses carry noindex; eligible public HTML, sites and files may permit indexing. " +
|
|
88
|
+
"Lowercase only: 1-32 of a-z, 0-9, hyphen.";
|
|
39
89
|
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
90
|
"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
91
|
const DELIVERY_MODE_SCHEMA = z.enum(["path", "subdomain"]).optional().describe("Optional delivery choice for a fresh publication, independent of password/namespace. " +
|
|
@@ -66,7 +116,38 @@ function noticeLines(data) {
|
|
|
66
116
|
return [];
|
|
67
117
|
return data.notices
|
|
68
118
|
.filter((n) => typeof n === "object" && n !== null && typeof n.code === "string" && typeof n.message === "string")
|
|
69
|
-
.map((n) =>
|
|
119
|
+
.map((n) => `${WARNING_CODES.has(n.code) ? "WARNING" : "Notice"} (${n.code}): ${n.message}`);
|
|
120
|
+
}
|
|
121
|
+
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.";
|
|
122
|
+
/** Owner token banner; the configured default is named, never repeated; none for a foreign existing publication.
|
|
123
|
+
* Sign-in hint only for a token minted by this call (not `sent`, not the configured default). */
|
|
124
|
+
function ownerTokenLines(data, purpose, sent) {
|
|
125
|
+
const token = data.ownerToken;
|
|
126
|
+
const extras = data.result?.extras;
|
|
127
|
+
if (extras?.isOwner === false)
|
|
128
|
+
return [``, `No owner token: an existing publication of another owner was returned.`];
|
|
129
|
+
if (typeof token !== "string" || token.length === 0)
|
|
130
|
+
return [];
|
|
131
|
+
const shown = ENV_OWNER_TOKEN !== undefined && token === ENV_OWNER_TOKEN ? `${OWNER_TOKEN_ENV} (not repeated)` : token;
|
|
132
|
+
return [
|
|
133
|
+
``,
|
|
134
|
+
`===================================`,
|
|
135
|
+
`OWNER TOKEN: ${shown}`,
|
|
136
|
+
`===================================`,
|
|
137
|
+
``,
|
|
138
|
+
`IMPORTANT: Save your owner token! You need it to ${purpose}. It cannot be recovered if lost.`,
|
|
139
|
+
...(token !== sent && token !== ENV_OWNER_TOKEN ? [SIGN_IN_HINT] : []),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
/** Private management link (present only when minted) and the public listing state. */
|
|
143
|
+
function ownerLinkLines(data) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
if (typeof data.manageUrl === "string" && data.manageUrl.length > 0) {
|
|
146
|
+
lines.push(``, `MANAGE LINK (private): ${data.manageUrl}`, `Give it to the user privately; never publish it.`);
|
|
147
|
+
}
|
|
148
|
+
if (data.listed === true)
|
|
149
|
+
lines.push(``, `VISIBILITY: public, listed. Hide: set_visibility listed=false`);
|
|
150
|
+
return lines;
|
|
70
151
|
}
|
|
71
152
|
/** Optional delivery fields on HTML and hosting responses: why the mode was chosen, flagged or not applied. */
|
|
72
153
|
function deliveryReasonLines(data) {
|
|
@@ -80,7 +161,7 @@ function deliveryReasonLines(data) {
|
|
|
80
161
|
}
|
|
81
162
|
async function apiRequest(method, path, body, headers, responseType = "json") {
|
|
82
163
|
const url = `${BASE_URL}${path}`;
|
|
83
|
-
const res = await
|
|
164
|
+
const res = await bpFetch(url, {
|
|
84
165
|
method,
|
|
85
166
|
headers: {
|
|
86
167
|
"Content-Type": "application/json",
|
|
@@ -91,7 +172,7 @@ async function apiRequest(method, path, body, headers, responseType = "json") {
|
|
|
91
172
|
const data = responseType === "text" ? await res.text() : await res.json().catch(() => null);
|
|
92
173
|
return { ok: res.ok, status: res.status, data, headers: res.headers };
|
|
93
174
|
}
|
|
94
|
-
function formatPublishResponse(data, effectiveNamespace) {
|
|
175
|
+
function formatPublishResponse(data, effectiveNamespace, sentOwnerToken) {
|
|
95
176
|
const lines = [
|
|
96
177
|
`Published successfully!`,
|
|
97
178
|
``,
|
|
@@ -103,7 +184,8 @@ function formatPublishResponse(data, effectiveNamespace) {
|
|
|
103
184
|
if (data.expiresAt)
|
|
104
185
|
lines.push(`Expires: ${data.expiresAt}`);
|
|
105
186
|
lines.push(...hostingResponseLines(data));
|
|
106
|
-
lines.push(
|
|
187
|
+
lines.push(...ownerTokenLines(data, "update or delete this resource later", sentOwnerToken));
|
|
188
|
+
lines.push(...ownerLinkLines(data));
|
|
107
189
|
const ns = (data.namespace ?? effectiveNamespace) || "";
|
|
108
190
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
109
191
|
lines.push(``, data.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
@@ -128,7 +210,8 @@ function formatUpdateResponse(data) {
|
|
|
128
210
|
}
|
|
129
211
|
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
212
|
"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."
|
|
213
|
+
"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. " +
|
|
214
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
132
215
|
content: z.string().describe("HTML or Markdown content to publish"),
|
|
133
216
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
134
217
|
format: z
|
|
@@ -154,7 +237,8 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repos
|
|
|
154
237
|
.boolean()
|
|
155
238
|
.optional()
|
|
156
239
|
.describe("Add a thin top toolbar (filename + Download button + theme toggle) on the served page. Default: hidden."),
|
|
157
|
-
|
|
240
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
241
|
+
}, async ({ content, format, namespace, password, ttlDays, filename, showTopBar, deliveryMode, ownerToken }) => {
|
|
158
242
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
159
243
|
const qsParams = [`ns=${encodeURIComponent(effectiveNamespace)}`, `format=${encodeURIComponent(format.toLowerCase())}`];
|
|
160
244
|
if (ttlDays !== undefined)
|
|
@@ -169,6 +253,9 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repos
|
|
|
169
253
|
headers["X-Password"] = password;
|
|
170
254
|
if (deliveryMode)
|
|
171
255
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
256
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
257
|
+
if (owner)
|
|
258
|
+
headers["X-Owner-Token"] = owner;
|
|
172
259
|
const { ok, data } = await apiRequest("POST", `/api/html?${qsParams.join("&")}`, body, headers);
|
|
173
260
|
if (!ok) {
|
|
174
261
|
return {
|
|
@@ -185,12 +272,13 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage (repos
|
|
|
185
272
|
content: [
|
|
186
273
|
{
|
|
187
274
|
type: "text",
|
|
188
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
275
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
189
276
|
},
|
|
190
277
|
],
|
|
191
278
|
};
|
|
192
279
|
});
|
|
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."
|
|
280
|
+
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. " +
|
|
281
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
194
282
|
url: z.string().url().describe("URL of the file to upload"),
|
|
195
283
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
196
284
|
namespace: z
|
|
@@ -198,7 +286,8 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defau
|
|
|
198
286
|
.optional()
|
|
199
287
|
.describe(NAMESPACE_DESCRIPTION),
|
|
200
288
|
filename: z.string().optional().describe("Custom filename (optional)"),
|
|
201
|
-
|
|
289
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
290
|
+
}, async ({ url: fileUrl, namespace, filename, deliveryMode, ownerToken }) => {
|
|
202
291
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
203
292
|
const fileRes = await fetch(fileUrl);
|
|
204
293
|
if (!fileRes.ok) {
|
|
@@ -215,9 +304,15 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defau
|
|
|
215
304
|
const blob = await fileRes.blob();
|
|
216
305
|
const formData = new FormData();
|
|
217
306
|
formData.append("file", blob, filename || fileUrl.split("/").pop() || "file");
|
|
218
|
-
const
|
|
307
|
+
const headers = {};
|
|
308
|
+
if (deliveryMode)
|
|
309
|
+
headers["X-Delivery-Mode"] = deliveryMode;
|
|
310
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
311
|
+
if (owner)
|
|
312
|
+
headers["X-Owner-Token"] = owner;
|
|
313
|
+
const res = await bpFetch(`${BASE_URL}/api/files?ns=${encodeURIComponent(effectiveNamespace)}`, {
|
|
219
314
|
method: "POST",
|
|
220
|
-
headers
|
|
315
|
+
headers,
|
|
221
316
|
body: formData,
|
|
222
317
|
});
|
|
223
318
|
const data = await res.json().catch(() => null);
|
|
@@ -236,7 +331,7 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Repository defau
|
|
|
236
331
|
content: [
|
|
237
332
|
{
|
|
238
333
|
type: "text",
|
|
239
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
334
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
240
335
|
},
|
|
241
336
|
],
|
|
242
337
|
};
|
|
@@ -245,51 +340,36 @@ server.tool("update_hosting", "Change delivery mode for a NEW publication (not a
|
|
|
245
340
|
type: z.enum(["html", "file", "json", "kv"]).describe("Resource type; sites always stay on their own subdomain"),
|
|
246
341
|
namespace: z.string().describe("Resource namespace; non-public HTML-format pages stay on subdomain"),
|
|
247
342
|
id: z.string().describe("Resource ID"),
|
|
248
|
-
ownerToken: z.string().describe(
|
|
343
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
249
344
|
deliveryMode: z.enum(["path", "subdomain"]).describe("path = Promotion; subdomain = Dedicated subdomain"),
|
|
250
345
|
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
346
|
}, async ({ type, namespace, id, ownerToken, deliveryMode, expectedVersion }) => {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
}
|
|
347
|
+
const auth = ownerTokenFor(ownerToken);
|
|
348
|
+
if (!auth.token)
|
|
349
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
350
|
+
const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/hosting`;
|
|
351
|
+
const { ok, status, data } = await apiRequest("PATCH", path, { deliveryMode, expectedVersion }, { "X-Owner-Token": auth.token });
|
|
352
|
+
if (!ok)
|
|
353
|
+
return ownerFailure("update hosting", status, data, auth.fromEnv);
|
|
261
354
|
return {
|
|
262
355
|
content: [{ type: "text", text: formatUpdateResponse(data) }],
|
|
263
356
|
};
|
|
264
357
|
});
|
|
265
|
-
server.tool("delete_resource", "Delete a BrewPage resource (HTML page, KV store, JSON
|
|
358
|
+
server.tool("delete_resource", "Delete a BrewPage resource (HTML page, site, KV store, JSON document, or file) using the owner token.", {
|
|
266
359
|
type: z
|
|
267
|
-
.enum(["html", "kv", "json", "file"])
|
|
360
|
+
.enum(["html", "site", "kv", "json", "file"])
|
|
268
361
|
.describe("Resource type to delete"),
|
|
269
362
|
namespace: z.string().describe("Resource namespace"),
|
|
270
363
|
id: z.string().describe("Resource ID"),
|
|
271
|
-
ownerToken: z
|
|
272
|
-
.string()
|
|
273
|
-
.describe("Owner token received when the resource was created"),
|
|
364
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
274
365
|
}, 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
|
-
}
|
|
366
|
+
const auth = ownerTokenFor(ownerToken);
|
|
367
|
+
if (!auth.token)
|
|
368
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
369
|
+
const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
|
|
370
|
+
const { ok, status, data } = await apiRequest("DELETE", path, undefined, { "X-Owner-Token": auth.token });
|
|
371
|
+
if (!ok)
|
|
372
|
+
return ownerFailure("delete", status, data, auth.fromEnv);
|
|
293
373
|
return {
|
|
294
374
|
content: [
|
|
295
375
|
{
|
|
@@ -372,7 +452,8 @@ server.tool("get_stats", "Get BrewPage platform-wide usage statistics including
|
|
|
372
452
|
],
|
|
373
453
|
};
|
|
374
454
|
});
|
|
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
|
|
455
|
+
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 +
|
|
456
|
+
" " + MANAGE_URL_TEXT + " " + SECRETS, {
|
|
376
457
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
377
458
|
entryContent: z
|
|
378
459
|
.string()
|
|
@@ -395,10 +476,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
395
476
|
.optional()
|
|
396
477
|
.describe("Optional password to protect the site"),
|
|
397
478
|
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"),
|
|
479
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
402
480
|
}, async ({ entryContent, files, entry, namespace, password, ttlDays, ownerToken, deliveryMode }) => {
|
|
403
481
|
const hasEntry = typeof entryContent === "string";
|
|
404
482
|
const hasFiles = Array.isArray(files) && files.length > 0;
|
|
@@ -429,8 +507,9 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
429
507
|
const headers = {};
|
|
430
508
|
if (password)
|
|
431
509
|
headers["X-Password"] = password;
|
|
432
|
-
|
|
433
|
-
|
|
510
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
511
|
+
if (owner)
|
|
512
|
+
headers["X-Owner-Token"] = owner;
|
|
434
513
|
if (deliveryMode)
|
|
435
514
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
436
515
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
@@ -442,7 +521,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
442
521
|
qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
|
|
443
522
|
const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
|
|
444
523
|
const url = `${BASE_URL}/api/sites${qs}`;
|
|
445
|
-
const res = await
|
|
524
|
+
const res = await bpFetch(url, { method: "POST", headers, body: formData });
|
|
446
525
|
const data = await res.json().catch(() => null);
|
|
447
526
|
if (!res.ok) {
|
|
448
527
|
return {
|
|
@@ -474,7 +553,8 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
474
553
|
if (site.expiresAt)
|
|
475
554
|
lines.push(`Expires: ${site.expiresAt}`);
|
|
476
555
|
lines.push(...hostingResponseLines(site));
|
|
477
|
-
lines.push(
|
|
556
|
+
lines.push(...ownerTokenLines(site, "view info or delete this site", owner));
|
|
557
|
+
lines.push(...ownerLinkLines(site));
|
|
478
558
|
const ns = (site.namespace || effectiveNamespace) || "";
|
|
479
559
|
if (ns !== PUBLIC_NAMESPACE) {
|
|
480
560
|
lines.push(``, site.access === "protected" ? PROTECTED_UNLISTED_NOTICE : UNLISTED_NOTICE);
|
|
@@ -486,9 +566,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
|
|
|
486
566
|
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, {
|
|
487
567
|
namespace: z.string().describe("Site namespace"),
|
|
488
568
|
id: z.string().describe("Site ID"),
|
|
489
|
-
ownerToken: z
|
|
490
|
-
.string()
|
|
491
|
-
.describe("Owner token received when the site was created (required for republish)"),
|
|
569
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
492
570
|
entryContent: z
|
|
493
571
|
.string()
|
|
494
572
|
.optional()
|
|
@@ -533,7 +611,10 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
533
611
|
formData.append("files", entryBlob, entryName);
|
|
534
612
|
formData.append("paths", entryName);
|
|
535
613
|
}
|
|
536
|
-
const
|
|
614
|
+
const auth = ownerTokenFor(ownerToken);
|
|
615
|
+
if (!auth.token)
|
|
616
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
617
|
+
const headers = { "X-Owner-Token": auth.token };
|
|
537
618
|
const qsParams = [];
|
|
538
619
|
if (entry)
|
|
539
620
|
qsParams.push(`entry=${encodeURIComponent(entry)}`);
|
|
@@ -543,19 +624,10 @@ server.tool("republish_site", "Republish (full-replace) an existing multi-file B
|
|
|
543
624
|
qsParams.push(`tags=${encodeURIComponent(tags.join(","))}`);
|
|
544
625
|
const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
|
|
545
626
|
const url = `${BASE_URL}/api/sites/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}${qs}`;
|
|
546
|
-
const res = await
|
|
627
|
+
const res = await bpFetch(url, { method: "PUT", headers, body: formData });
|
|
547
628
|
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
|
-
}
|
|
629
|
+
if (!res.ok)
|
|
630
|
+
return ownerFailure("republish site", res.status, data, auth.fromEnv);
|
|
559
631
|
const site = data;
|
|
560
632
|
const lines = [
|
|
561
633
|
`Site republished successfully (full replace)!`,
|
|
@@ -588,9 +660,7 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
588
660
|
namespace: z.string().describe("Page namespace"),
|
|
589
661
|
id: z.string().describe("Page ID"),
|
|
590
662
|
content: z.string().describe("New HTML or Markdown content"),
|
|
591
|
-
ownerToken: z
|
|
592
|
-
.string()
|
|
593
|
-
.describe("Owner token received when the page was created"),
|
|
663
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
594
664
|
format: z
|
|
595
665
|
.string()
|
|
596
666
|
.optional()
|
|
@@ -599,21 +669,15 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
599
669
|
const body = { content };
|
|
600
670
|
if (format)
|
|
601
671
|
body.format = format;
|
|
672
|
+
const auth = ownerTokenFor(ownerToken);
|
|
673
|
+
if (!auth.token)
|
|
674
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
602
675
|
const path = `/api/html/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
|
|
603
676
|
const { ok, status, data } = await apiRequest("PUT", path, body, {
|
|
604
|
-
"X-Owner-Token":
|
|
677
|
+
"X-Owner-Token": auth.token,
|
|
605
678
|
});
|
|
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
|
-
}
|
|
679
|
+
if (!ok)
|
|
680
|
+
return ownerFailure("update HTML", status, data, auth.fromEnv);
|
|
617
681
|
return {
|
|
618
682
|
content: [
|
|
619
683
|
{
|
|
@@ -623,7 +687,8 @@ server.tool("update_html", "Update an existing BrewPage HTML/Markdown page. Requ
|
|
|
623
687
|
],
|
|
624
688
|
};
|
|
625
689
|
});
|
|
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."
|
|
690
|
+
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. " +
|
|
691
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
627
692
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
628
693
|
json: z
|
|
629
694
|
.union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
|
|
@@ -641,10 +706,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage (repository def
|
|
|
641
706
|
.array(z.string())
|
|
642
707
|
.optional()
|
|
643
708
|
.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"),
|
|
709
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
648
710
|
}, async ({ json, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
649
711
|
const jsonString = typeof json === "string" ? json : JSON.stringify(json);
|
|
650
712
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
@@ -660,11 +722,12 @@ server.tool("publish_json", "Publish a JSON document to BrewPage (repository def
|
|
|
660
722
|
const headers = { "Content-Type": "application/json" };
|
|
661
723
|
if (password)
|
|
662
724
|
headers["X-Password"] = password;
|
|
663
|
-
|
|
664
|
-
|
|
725
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
726
|
+
if (owner)
|
|
727
|
+
headers["X-Owner-Token"] = owner;
|
|
665
728
|
if (deliveryMode)
|
|
666
729
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
667
|
-
const res = await
|
|
730
|
+
const res = await bpFetch(`${BASE_URL}/api/json${qs}`, {
|
|
668
731
|
method: "POST",
|
|
669
732
|
headers,
|
|
670
733
|
body: jsonString,
|
|
@@ -685,7 +748,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage (repository def
|
|
|
685
748
|
content: [
|
|
686
749
|
{
|
|
687
750
|
type: "text",
|
|
688
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
751
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
689
752
|
},
|
|
690
753
|
],
|
|
691
754
|
};
|
|
@@ -729,32 +792,24 @@ server.tool("update_json", "Update an existing BrewPage JSON document. Requires
|
|
|
729
792
|
json: z
|
|
730
793
|
.union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
|
|
731
794
|
.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"),
|
|
795
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
735
796
|
}, async ({ namespace, id, json, ownerToken }) => {
|
|
736
797
|
const jsonString = typeof json === "string" ? json : JSON.stringify(json);
|
|
798
|
+
const auth = ownerTokenFor(ownerToken);
|
|
799
|
+
if (!auth.token)
|
|
800
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
737
801
|
const url = `${BASE_URL}/api/json/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
|
|
738
|
-
const res = await
|
|
802
|
+
const res = await bpFetch(url, {
|
|
739
803
|
method: "PUT",
|
|
740
804
|
headers: {
|
|
741
805
|
"Content-Type": "application/json",
|
|
742
|
-
"X-Owner-Token":
|
|
806
|
+
"X-Owner-Token": auth.token,
|
|
743
807
|
},
|
|
744
808
|
body: jsonString,
|
|
745
809
|
});
|
|
746
810
|
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
|
-
}
|
|
811
|
+
if (!res.ok)
|
|
812
|
+
return ownerFailure("update JSON", res.status, data, auth.fromEnv);
|
|
758
813
|
return {
|
|
759
814
|
content: [
|
|
760
815
|
{
|
|
@@ -764,7 +819,8 @@ server.tool("update_json", "Update an existing BrewPage JSON document. Requires
|
|
|
764
819
|
],
|
|
765
820
|
};
|
|
766
821
|
});
|
|
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."
|
|
822
|
+
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. " +
|
|
823
|
+
MANAGE_URL_TEXT + " " + SECRETS, {
|
|
768
824
|
deliveryMode: DELIVERY_MODE_SCHEMA,
|
|
769
825
|
key: z.string().describe("KV key (first part of the (namespace, id, key) addressing tuple)"),
|
|
770
826
|
value: z.string().describe("KV value"),
|
|
@@ -781,10 +837,7 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
781
837
|
.array(z.string())
|
|
782
838
|
.optional()
|
|
783
839
|
.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"),
|
|
840
|
+
ownerToken: z.string().optional().describe(CREATE_OWNER_TOKEN_DESCRIPTION),
|
|
788
841
|
}, async ({ key, value, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
|
|
789
842
|
const body = { key, value };
|
|
790
843
|
const effectiveNamespace = resolveNamespace(namespace);
|
|
@@ -800,8 +853,9 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
800
853
|
const headers = {};
|
|
801
854
|
if (password)
|
|
802
855
|
headers["X-Password"] = password;
|
|
803
|
-
|
|
804
|
-
|
|
856
|
+
const owner = ownerTokenFor(ownerToken).token;
|
|
857
|
+
if (owner)
|
|
858
|
+
headers["X-Owner-Token"] = owner;
|
|
805
859
|
if (deliveryMode)
|
|
806
860
|
headers["X-Delivery-Mode"] = deliveryMode;
|
|
807
861
|
const { ok, status, data } = await apiRequest("POST", `/api/kv${qs}`, body, headers);
|
|
@@ -820,36 +874,28 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
|
|
|
820
874
|
content: [
|
|
821
875
|
{
|
|
822
876
|
type: "text",
|
|
823
|
-
text: formatPublishResponse(data, effectiveNamespace),
|
|
877
|
+
text: formatPublishResponse(data, effectiveNamespace, owner),
|
|
824
878
|
},
|
|
825
879
|
],
|
|
826
880
|
};
|
|
827
881
|
});
|
|
828
|
-
server.tool("set_kv", "Set or update a key in an existing BrewPage KV store. Requires owner token.", {
|
|
882
|
+
server.tool("set_kv", "Set or update a key in an existing BrewPage KV store. Requires owner token. The key name `hosting` is reserved.", {
|
|
829
883
|
namespace: z.string().describe("KV store namespace"),
|
|
830
884
|
id: z.string().describe("KV store ID"),
|
|
831
885
|
key: z.string().describe("Key to set"),
|
|
832
886
|
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"),
|
|
887
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
836
888
|
}, async ({ namespace, id, key, value, ownerToken }) => {
|
|
837
889
|
const body = { value };
|
|
890
|
+
const auth = ownerTokenFor(ownerToken);
|
|
891
|
+
if (!auth.token)
|
|
892
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
838
893
|
const path = `/api/kv/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/${encodeURIComponent(key)}`;
|
|
839
894
|
const { ok, status, data } = await apiRequest("PUT", path, body, {
|
|
840
|
-
"X-Owner-Token":
|
|
895
|
+
"X-Owner-Token": auth.token,
|
|
841
896
|
});
|
|
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
|
-
}
|
|
897
|
+
if (!ok)
|
|
898
|
+
return ownerFailure("set KV", status, data, auth.fromEnv);
|
|
853
899
|
return {
|
|
854
900
|
content: [
|
|
855
901
|
{
|
|
@@ -893,10 +939,11 @@ server.tool("get_kv", "Fetch a value from a BrewPage KV store by namespace, ID,
|
|
|
893
939
|
],
|
|
894
940
|
};
|
|
895
941
|
});
|
|
896
|
-
server.tool("get_limits", "Get the current BrewPage upload limits (GET /api/limits
|
|
942
|
+
server.tool("get_limits", "Get the current BrewPage upload limits (GET /api/limits): request size, site file count, per-file and total size, " +
|
|
897
943
|
"ZIP expansion, path and name lengths, TTL default/maximum, allowed file extensions and skipped system files. Sizes are bytes. " +
|
|
898
|
-
|
|
899
|
-
const
|
|
944
|
+
`With ${OWNER_TOKEN_ENV} set: also reach (token|profile) and ownerMaxTtlDays. Call it before publishing a large or multi-file site.`, {}, async () => {
|
|
945
|
+
const headers = ENV_OWNER_TOKEN ? { "X-Owner-Token": ENV_OWNER_TOKEN } : {};
|
|
946
|
+
const { ok, status, data } = await apiRequest("GET", "/api/limits", undefined, headers);
|
|
900
947
|
if (!ok) {
|
|
901
948
|
return {
|
|
902
949
|
content: [
|
|
@@ -943,8 +990,11 @@ server.tool("search_gallery", "Search the BrewPage public gallery. Optional filt
|
|
|
943
990
|
ownerToken: z
|
|
944
991
|
.string()
|
|
945
992
|
.optional()
|
|
946
|
-
.describe(
|
|
993
|
+
.describe(`Owner token; only sent as X-Owner-Token when 'mine' is true. Default: ${OWNER_TOKEN_ENV}.`),
|
|
947
994
|
}, async ({ q, page, size, sort, mine, ownerToken }) => {
|
|
995
|
+
const auth = ownerTokenFor(ownerToken);
|
|
996
|
+
if (mine === true && !auth.token)
|
|
997
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
948
998
|
const qsParams = [];
|
|
949
999
|
if (q !== undefined)
|
|
950
1000
|
qsParams.push(`q=${encodeURIComponent(q)}`);
|
|
@@ -958,8 +1008,8 @@ server.tool("search_gallery", "Search the BrewPage public gallery. Optional filt
|
|
|
958
1008
|
qsParams.push("mine=true");
|
|
959
1009
|
const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
|
|
960
1010
|
const headers = {};
|
|
961
|
-
if (mine === true &&
|
|
962
|
-
headers["X-Owner-Token"] =
|
|
1011
|
+
if (mine === true && auth.token)
|
|
1012
|
+
headers["X-Owner-Token"] = auth.token;
|
|
963
1013
|
const { ok, status, data } = await apiRequest("GET", `/api/gallery${qs}`, undefined, headers);
|
|
964
1014
|
if (!ok) {
|
|
965
1015
|
return {
|
|
@@ -981,6 +1031,59 @@ server.tool("search_gallery", "Search the BrewPage public gallery. Optional filt
|
|
|
981
1031
|
],
|
|
982
1032
|
};
|
|
983
1033
|
});
|
|
1034
|
+
server.tool("set_visibility", "Hide or show a public publication in gallery & search. Link unchanged.", {
|
|
1035
|
+
type: z.enum(["html", "site", "file", "json", "kv"]).describe("Publication type; only public html, site and file can be listed"),
|
|
1036
|
+
namespace: z.string().describe("Publication namespace"),
|
|
1037
|
+
id: z.string().describe("Publication ID"),
|
|
1038
|
+
listed: z.boolean().describe("false = hide, true = show"),
|
|
1039
|
+
ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
|
|
1040
|
+
}, async ({ type, namespace, id, listed, ownerToken }) => {
|
|
1041
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1042
|
+
if (!auth.token)
|
|
1043
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
1044
|
+
const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/visibility`;
|
|
1045
|
+
const { ok, status, data } = await apiRequest("POST", path, { listed }, { "X-Owner-Token": auth.token });
|
|
1046
|
+
if (!ok)
|
|
1047
|
+
return ownerFailure("set visibility", status, data, auth.fromEnv);
|
|
1048
|
+
const result = data;
|
|
1049
|
+
return textResult([`LISTED: ${result.listed} · link unchanged`, ...noticeLines(result)].join("\n"));
|
|
1050
|
+
});
|
|
1051
|
+
server.tool("bind_token", "Add an owner token to your BrewPage profile (as publish). Needs an admin token of that profile.", {
|
|
1052
|
+
token: z.string().describe("Owner token to add"),
|
|
1053
|
+
ownerToken: z.string().optional().describe(`Admin token of the profile. Default: ${OWNER_TOKEN_ENV}.`),
|
|
1054
|
+
}, async ({ token, ownerToken }) => {
|
|
1055
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1056
|
+
if (!auth.token)
|
|
1057
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
1058
|
+
const { ok, status, data } = await apiRequest("POST", "/api/owner-token/bind", { token }, { "X-Owner-Token": auth.token });
|
|
1059
|
+
if (!ok)
|
|
1060
|
+
return textResult(`Failed to bind token (${status}): ${JSON.stringify(data)}`, true);
|
|
1061
|
+
const result = data;
|
|
1062
|
+
return textResult(`BOUND ${maskToken(token)} · ${result.active}/${result.max} active`);
|
|
1063
|
+
});
|
|
1064
|
+
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.", {
|
|
1065
|
+
label: z.string().optional().describe("Optional label shown in the profile"),
|
|
1066
|
+
scope: z.enum(["publish", "admin"]).optional().describe("publish (default) or admin (can manage tokens)"),
|
|
1067
|
+
expiresIn: z.enum(["30d", "90d", "1y", "never"]).optional().describe("Default: admin 90d, publish never"),
|
|
1068
|
+
ownerToken: z.string().optional().describe(`Admin token of the profile. Default: ${OWNER_TOKEN_ENV}.`),
|
|
1069
|
+
}, async ({ label, scope, expiresIn, ownerToken }) => {
|
|
1070
|
+
const auth = ownerTokenFor(ownerToken);
|
|
1071
|
+
if (!auth.token)
|
|
1072
|
+
return textResult(NO_OWNER_TOKEN, true);
|
|
1073
|
+
const { ok, status, data } = await apiRequest("POST", "/api/owner-token/mint", { label, scope, expiresIn }, { "X-Owner-Token": auth.token });
|
|
1074
|
+
if (!ok)
|
|
1075
|
+
return textResult(`Failed to mint token (${status}): ${JSON.stringify(data)}`, true);
|
|
1076
|
+
const result = data;
|
|
1077
|
+
return textResult([
|
|
1078
|
+
`MINTED · ${result.scope} · expires ${result.expiresAt ?? "never"}`,
|
|
1079
|
+
``,
|
|
1080
|
+
`===================================`,
|
|
1081
|
+
`NEW TOKEN (shown once): ${result.token}`,
|
|
1082
|
+
`===================================`,
|
|
1083
|
+
``,
|
|
1084
|
+
`Save it now; it cannot be shown again. Never publish it.`,
|
|
1085
|
+
].join("\n"));
|
|
1086
|
+
});
|
|
984
1087
|
async function main() {
|
|
985
1088
|
const transport = new StdioServerTransport();
|
|
986
1089
|
await server.connect(transport);
|
package/package.json
CHANGED