brewpage-mcp 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +12 -6
  2. package/dist/index.js +161 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -92,7 +92,7 @@ Fourteen tools are available, grouped by resource: **HTML**, **JSON**, **KV**, *
92
92
 
93
93
  Publish HTML or Markdown content to BrewPage. Returns a public URL and owner token.
94
94
 
95
- Parameters: `content` (string), `format` (`HTML` | `MARKDOWN`, default `HTML`), `namespace` (optional, defaults to `public`), `password` (optional), `ttlDays` (1--30, default 15), `filename` (optional, used as title fallback), `showTopBar` (optional boolean -- adds a toolbar with filename, Download button, and theme toggle).
95
+ Parameters: `content` (string), `format` (`HTML` | `MARKDOWN`, default `HTML`), `namespace` (optional -- **omit to keep the page private/unlisted**: reachable only by its link, not in the gallery, not indexed by search engines; pass `public` only when the user explicitly wants it gallery-listed and search-indexed), `password` (optional), `ttlDays` (1--30, default 15), `filename` (optional, used as title fallback), `showTopBar` (optional boolean -- adds a toolbar with filename, Download button, and theme toggle).
96
96
 
97
97
  Example prompt that invokes this tool:
98
98
 
@@ -142,7 +142,7 @@ get_page(namespace="public", id="aBcDeFgHiJ")
142
142
 
143
143
  Upload a file to BrewPage by fetching it from a URL. Returns a public URL and owner token. Supports images, PDFs, video, audio, code files, and archives.
144
144
 
145
- Parameters: `url` (string, the source URL to fetch), `namespace` (optional), `filename` (optional custom filename).
145
+ Parameters: `url` (string, the source URL to fetch), `namespace` (optional -- omit to keep the file private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `filename` (optional custom filename).
146
146
 
147
147
  Example prompt:
148
148
 
@@ -160,7 +160,7 @@ publish_file(url="https://example.com/diagram.png")
160
160
 
161
161
  Publish a single-page or multi-file HTML site. Pass `entryContent` for a single page or `files` (array of `{path, content}`) for a multi-file site. Supports password protection, TTL, and owner token grouping.
162
162
 
163
- Parameters: `entryContent` (string, mutually exclusive with `files`), `files` (array of `{path, content}`, mutually exclusive with `entryContent`), `entry` (optional entry file path, default `index.html`), `namespace` (optional), `password` (optional), `ttlDays` (1--30, default 15), `ownerToken` (optional, groups site under an existing owner).
163
+ Parameters: `entryContent` (string, mutually exclusive with `files`), `files` (array of `{path, content}`, mutually exclusive with `entryContent`), `entry` (optional entry file path, default `index.html`), `namespace` (optional -- omit to keep the site private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `password` (optional), `ttlDays` (1--30, default 15), `ownerToken` (optional, groups site under an existing owner).
164
164
 
165
165
  Example prompt:
166
166
 
@@ -178,7 +178,7 @@ publish_site(files=[{"path": "index.html", "content": "..."}, {"path": "style.cs
178
178
 
179
179
  Publish a JSON document to BrewPage. Returns a public URL and owner token. The body may be passed as a JSON string or as a structured object.
180
180
 
181
- Parameters: `json` (string or object, the JSON payload), `namespace` (optional, defaults to `public`), `password` (optional), `ttlDays` (optional, 1--30, default 15), `tags` (optional `string[]`).
181
+ Parameters: `json` (string or object, the JSON payload), `namespace` (optional -- omit to keep the document private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `password` (optional), `ttlDays` (optional, 1--30, default 15), `tags` (optional `string[]`).
182
182
 
183
183
  Example prompt:
184
184
 
@@ -228,7 +228,7 @@ update_json(namespace="public", id="aBcDeFgHiJ", json={"mode": "dark", "version"
228
228
 
229
229
  Create a new KV (key/value) entry. Returns a public URL and owner token. The KV entry is addressed by `(namespace, id, key)`; this tool creates the entry and emits the generated `id` along with the owner token.
230
230
 
231
- Parameters: `key` (string), `value` (string), `namespace` (optional, defaults to `public`), `password` (optional), `ttlDays` (optional, 1--30, default 15), `tags` (optional `string[]`).
231
+ Parameters: `key` (string), `value` (string), `namespace` (optional -- omit to keep the KV store private/unlisted: reachable only by its link, not in the gallery, not search-indexed; pass `public` only to list and index it), `password` (optional), `ttlDays` (optional, 1--30, default 15), `tags` (optional `string[]`).
232
232
 
233
233
  Example prompt:
234
234
 
@@ -328,7 +328,7 @@ get_stats()
328
328
  |--------------------|------|-------|
329
329
  | "Publish this HTML so I can share it" | `publish_html` | `format=HTML`; save `ownerToken` |
330
330
  | "Share these meeting notes as a link" | `publish_html` | `format=MARKDOWN`; readable rendered output |
331
- | "Host this AI-generated artifact" | `publish_html` | `namespace` optional; public by default |
331
+ | "Host this AI-generated artifact" | `publish_html` | `namespace` optional; **private/unlisted by default** -- pass `public` to gallery-list + index |
332
332
  | "Upload this image / PDF / video" | `publish_file` | Fetches from URL; inline preview on short URL |
333
333
  | "Deploy this static site" | `publish_site` | Pass `files` array or `entryContent` |
334
334
  | "Fix a typo in the page I shared -- same link" | `update_html` | Requires `ownerToken`; URL stays the same |
@@ -371,6 +371,12 @@ Every publish response includes an **owner token** -- the only credential that a
371
371
 
372
372
  ## Changelog
373
373
 
374
+ ## 1.6.0 -- 2026-06-05
375
+
376
+ - **Private-by-default publishing.** When `namespace` is omitted, all five publish tools (`publish_html`, `publish_file`, `publish_site`, `publish_json`, `publish_kv`) now send an explicit unlisted namespace (`priv-<random>`) instead of letting the backend default to `public`. Omitted namespace = unlisted (reachable only by its link, not gallery-listed, not search-indexed); pass `namespace: "public"` explicitly to list + index. Non-breaking: `namespace` stays optional.
377
+ - Publish responses for non-public namespaces now append an "Unlisted link" notice explaining how to make content public.
378
+ - Tool descriptions and README rewritten to reflect the private-by-default model.
379
+
374
380
  ## 1.5.0 -- 2026-05-21
375
381
 
376
382
  - Add 8 new MCP tools: `update_html`, `publish_json`, `get_json`, `update_json`, `publish_kv`, `set_kv`, `get_kv`, `search_gallery`.
package/dist/index.js CHANGED
@@ -5,8 +5,36 @@ import { z } from "zod";
5
5
  const BASE_URL = process.env.BREWPAGE_URL || "https://brewpage.app";
6
6
  const server = new McpServer({
7
7
  name: "brewpage-mcp",
8
- version: "1.5.1",
8
+ version: "1.7.0",
9
9
  });
10
+ const PUBLIC_NAMESPACE = "public";
11
+ /**
12
+ * Generate a short, unlisted private namespace slug used when the caller omits
13
+ * `namespace`. Sending an explicit non-public ns prevents the backend from
14
+ * applying its `public` default, which would gallery-list + index the content.
15
+ * Format: `priv-<6 lowercase-alphanumeric chars>`.
16
+ */
17
+ function defaultPrivateNamespace() {
18
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
19
+ let slug = "";
20
+ for (let i = 0; i < 6; i++) {
21
+ slug += alphabet[Math.floor(Math.random() * alphabet.length)];
22
+ }
23
+ return `priv-${slug}`;
24
+ }
25
+ /**
26
+ * Resolve the effective namespace actually sent to the API: the caller's value
27
+ * verbatim when provided (including `public`), otherwise a generated private slug.
28
+ */
29
+ function resolveNamespace(namespace) {
30
+ return namespace && namespace.length > 0 ? namespace : defaultPrivateNamespace();
31
+ }
32
+ const UNLISTED_NOTICE = "Unlisted link — anyone who has it can open it, but it's not in the gallery or search. " +
33
+ "Publish with namespace `public` to list it in the gallery and have search engines index it.";
34
+ const NAMESPACE_DESCRIPTION = "Optional. Omit to keep the content PRIVATE/unlisted (reachable only by its link — " +
35
+ "not listed in the gallery and not indexed by search engines). Pass a custom namespace " +
36
+ "to group private content, or pass `public` ONLY when the user explicitly wants the page " +
37
+ "listed in the brewpage.app gallery and indexed by search engines.";
10
38
  async function apiRequest(method, path, body, headers) {
11
39
  const url = `${BASE_URL}${path}`;
12
40
  const res = await fetch(url, {
@@ -20,7 +48,7 @@ async function apiRequest(method, path, body, headers) {
20
48
  const data = await res.json().catch(() => null);
21
49
  return { ok: res.ok, status: res.status, data };
22
50
  }
23
- function formatPublishResponse(data) {
51
+ function formatPublishResponse(data, effectiveNamespace) {
24
52
  const lines = [
25
53
  `Published successfully!`,
26
54
  ``,
@@ -32,6 +60,10 @@ function formatPublishResponse(data) {
32
60
  if (data.expiresAt)
33
61
  lines.push(`Expires: ${data.expiresAt}`);
34
62
  lines.push(``, `===================================`, `OWNER TOKEN: ${data.ownerToken}`, `===================================`, ``, `IMPORTANT: Save your owner token! You need it to update or delete this resource later. It cannot be recovered if lost.`);
63
+ const ns = (effectiveNamespace ?? data.namespace) || "";
64
+ if (ns !== PUBLIC_NAMESPACE) {
65
+ lines.push(``, UNLISTED_NOTICE);
66
+ }
35
67
  return lines.join("\n");
36
68
  }
37
69
  function formatUpdateResponse(data) {
@@ -58,7 +90,7 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
58
90
  namespace: z
59
91
  .string()
60
92
  .optional()
61
- .describe("Namespace for the page (optional, defaults to 'public')"),
93
+ .describe(NAMESPACE_DESCRIPTION),
62
94
  password: z
63
95
  .string()
64
96
  .optional()
@@ -80,9 +112,9 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
80
112
  .optional()
81
113
  .describe("Add a thin top toolbar (filename + Download button + theme toggle) on the served page. Default: hidden."),
82
114
  }, async ({ content, format, namespace, password, ttlDays, filename, showTopBar }) => {
115
+ const effectiveNamespace = resolveNamespace(namespace);
83
116
  const body = { content, format };
84
- if (namespace)
85
- body.namespace = namespace;
117
+ body.namespace = effectiveNamespace;
86
118
  if (password)
87
119
  body.password = password;
88
120
  if (ttlDays)
@@ -107,7 +139,7 @@ server.tool("publish_html", "Publish HTML or Markdown content to BrewPage. Retur
107
139
  content: [
108
140
  {
109
141
  type: "text",
110
- text: formatPublishResponse(data),
142
+ text: formatPublishResponse(data, effectiveNamespace),
111
143
  },
112
144
  ],
113
145
  };
@@ -117,9 +149,10 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
117
149
  namespace: z
118
150
  .string()
119
151
  .optional()
120
- .describe("Namespace for the file (optional, defaults to 'public')"),
152
+ .describe(NAMESPACE_DESCRIPTION),
121
153
  filename: z.string().optional().describe("Custom filename (optional)"),
122
154
  }, async ({ url: fileUrl, namespace, filename }) => {
155
+ const effectiveNamespace = resolveNamespace(namespace);
123
156
  const fileRes = await fetch(fileUrl);
124
157
  if (!fileRes.ok) {
125
158
  return {
@@ -135,8 +168,7 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
135
168
  const blob = await fileRes.blob();
136
169
  const formData = new FormData();
137
170
  formData.append("file", blob, filename || fileUrl.split("/").pop() || "file");
138
- if (namespace)
139
- formData.append("namespace", namespace);
171
+ formData.append("namespace", effectiveNamespace);
140
172
  const res = await fetch(`${BASE_URL}/api/files`, {
141
173
  method: "POST",
142
174
  body: formData,
@@ -157,7 +189,7 @@ server.tool("publish_file", "Upload a file to BrewPage via URL. Returns a public
157
189
  content: [
158
190
  {
159
191
  type: "text",
160
- text: formatPublishResponse(data),
192
+ text: formatPublishResponse(data, effectiveNamespace),
161
193
  },
162
194
  ],
163
195
  };
@@ -280,7 +312,7 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
280
312
  namespace: z
281
313
  .string()
282
314
  .optional()
283
- .describe("Namespace for the site (optional, defaults to 'public')"),
315
+ .describe(NAMESPACE_DESCRIPTION),
284
316
  password: z
285
317
  .string()
286
318
  .optional()
@@ -327,11 +359,11 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
327
359
  headers["X-Password"] = password;
328
360
  if (ownerToken)
329
361
  headers["X-Owner-Token"] = ownerToken;
362
+ const effectiveNamespace = resolveNamespace(namespace);
330
363
  const qsParams = [];
331
364
  if (entry)
332
365
  qsParams.push(`entry=${encodeURIComponent(entry)}`);
333
- if (namespace)
334
- qsParams.push(`ns=${encodeURIComponent(namespace)}`);
366
+ qsParams.push(`ns=${encodeURIComponent(effectiveNamespace)}`);
335
367
  if (ttlDays)
336
368
  qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
337
369
  const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
@@ -368,6 +400,114 @@ server.tool("publish_site", "Publish an HTML site to BrewPage. Single-page: pass
368
400
  if (site.expiresAt)
369
401
  lines.push(`Expires: ${site.expiresAt}`);
370
402
  lines.push(``, `===================================`, `OWNER TOKEN: ${site.ownerToken}`, `===================================`, ``, `IMPORTANT: Save your owner token! You need it to view info or delete this site. It cannot be recovered if lost.`);
403
+ const ns = (site.namespace || effectiveNamespace) || "";
404
+ if (ns !== PUBLIC_NAMESPACE) {
405
+ lines.push(``, UNLISTED_NOTICE);
406
+ }
407
+ return {
408
+ content: [{ type: "text", text: lines.join("\n") }],
409
+ };
410
+ });
411
+ 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`.", {
412
+ namespace: z.string().describe("Site namespace"),
413
+ id: z.string().describe("Site ID"),
414
+ ownerToken: z
415
+ .string()
416
+ .describe("Owner token received when the site was created (required for republish)"),
417
+ entryContent: z
418
+ .string()
419
+ .optional()
420
+ .describe("HTML content for a single-page site (entry file). Mutually exclusive with `files`."),
421
+ files: z
422
+ .array(z.object({ path: z.string(), content: z.string() }))
423
+ .optional()
424
+ .describe("Multi-file site: array of `{path, content}`. Mutually exclusive with `entryContent`."),
425
+ entry: z
426
+ .string()
427
+ .optional()
428
+ .describe("Entry file path, default index.html"),
429
+ ttlDays: z
430
+ .number()
431
+ .min(1)
432
+ .max(30)
433
+ .optional()
434
+ .describe("Time to live in days (1-30, default: 15)"),
435
+ tags: z
436
+ .array(z.string())
437
+ .optional()
438
+ .describe("Optional tags (replaces the site's existing tags)"),
439
+ }, async ({ namespace, id, ownerToken, entryContent, files, entry, ttlDays, tags }) => {
440
+ const hasEntry = typeof entryContent === "string";
441
+ const hasFiles = Array.isArray(files) && files.length > 0;
442
+ if (hasEntry === hasFiles) {
443
+ return {
444
+ content: [
445
+ {
446
+ type: "text",
447
+ text: "Provide exactly one of `entryContent` (single-page) or `files` (multi-page).",
448
+ },
449
+ ],
450
+ isError: true,
451
+ };
452
+ }
453
+ const formData = new FormData();
454
+ if (hasFiles) {
455
+ for (const f of files) {
456
+ formData.append("files", new Blob([f.content], { type: "text/html" }), f.path);
457
+ formData.append("paths", f.path);
458
+ }
459
+ }
460
+ else {
461
+ const entryBlob = new Blob([entryContent], { type: "text/html" });
462
+ const entryName = entry || "index.html";
463
+ formData.append("files", entryBlob, entryName);
464
+ formData.append("paths", entryName);
465
+ }
466
+ const headers = { "X-Owner-Token": ownerToken };
467
+ const qsParams = [];
468
+ if (entry)
469
+ qsParams.push(`entry=${encodeURIComponent(entry)}`);
470
+ if (ttlDays)
471
+ qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
472
+ if (tags && tags.length)
473
+ qsParams.push(`tags=${encodeURIComponent(tags.join(","))}`);
474
+ const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
475
+ const url = `${BASE_URL}/api/sites/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}${qs}`;
476
+ const res = await fetch(url, { method: "PUT", headers, body: formData });
477
+ const data = await res.json().catch(() => null);
478
+ if (!res.ok) {
479
+ return {
480
+ content: [
481
+ {
482
+ type: "text",
483
+ text: `Failed to republish site (${res.status}): ${JSON.stringify(data)}`,
484
+ },
485
+ ],
486
+ isError: true,
487
+ };
488
+ }
489
+ const site = data;
490
+ const lines = [
491
+ `Site republished successfully (full replace)!`,
492
+ ``,
493
+ `URL: ${site.link || "N/A"}`,
494
+ `Namespace: ${site.namespace || namespace}`,
495
+ `ID: ${site.id || id}`,
496
+ `Entry file: ${site.entryFile || "index.html"}`,
497
+ `Files: ${site.fileCount || 1}`,
498
+ ];
499
+ if (site.totalSizeBytes !== undefined)
500
+ lines.push(`Size: ${site.totalSizeBytes} bytes`);
501
+ if (site.ownerLink)
502
+ lines.push(`Owner link: ${site.ownerLink}`);
503
+ if (Array.isArray(site.tags) && site.tags.length)
504
+ lines.push(`Tags: ${site.tags.join(", ")}`);
505
+ if (site.expiresAt)
506
+ lines.push(`Expires: ${site.expiresAt}`);
507
+ if (site.result)
508
+ lines.push(`Result: ${site.result}`);
509
+ if (site.visibilityNotice)
510
+ lines.push(``, String(site.visibilityNotice));
371
511
  return {
372
512
  content: [{ type: "text", text: lines.join("\n") }],
373
513
  };
@@ -418,7 +558,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
418
558
  namespace: z
419
559
  .string()
420
560
  .optional()
421
- .describe("Namespace for the document (optional, defaults to 'public')"),
561
+ .describe(NAMESPACE_DESCRIPTION),
422
562
  password: z
423
563
  .string()
424
564
  .optional()
@@ -440,9 +580,9 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
440
580
  .describe("Existing owner token to group this document under the same owner as previous content"),
441
581
  }, async ({ json, namespace, password, ttlDays, tags, ownerToken }) => {
442
582
  const jsonString = typeof json === "string" ? json : JSON.stringify(json);
583
+ const effectiveNamespace = resolveNamespace(namespace);
443
584
  const qsParams = [];
444
- if (namespace)
445
- qsParams.push(`ns=${encodeURIComponent(namespace)}`);
585
+ qsParams.push(`ns=${encodeURIComponent(effectiveNamespace)}`);
446
586
  if (ttlDays !== undefined)
447
587
  qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
448
588
  if (tags && tags.length) {
@@ -476,7 +616,7 @@ server.tool("publish_json", "Publish a JSON document to BrewPage. Accepts a JSON
476
616
  content: [
477
617
  {
478
618
  type: "text",
479
- text: formatPublishResponse(data),
619
+ text: formatPublishResponse(data, effectiveNamespace),
480
620
  },
481
621
  ],
482
622
  };
@@ -561,7 +701,7 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
561
701
  namespace: z
562
702
  .string()
563
703
  .optional()
564
- .describe("Namespace for the KV store (optional, defaults to 'public')"),
704
+ .describe(NAMESPACE_DESCRIPTION),
565
705
  password: z
566
706
  .string()
567
707
  .optional()
@@ -583,9 +723,9 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
583
723
  .describe("Existing owner token to group this KV store under the same owner as previous content"),
584
724
  }, async ({ key, value, namespace, password, ttlDays, tags, ownerToken }) => {
585
725
  const body = { key, value };
726
+ const effectiveNamespace = resolveNamespace(namespace);
586
727
  const qsParams = [];
587
- if (namespace)
588
- qsParams.push(`ns=${encodeURIComponent(namespace)}`);
728
+ qsParams.push(`ns=${encodeURIComponent(effectiveNamespace)}`);
589
729
  if (ttlDays !== undefined)
590
730
  qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
591
731
  if (tags && tags.length) {
@@ -614,7 +754,7 @@ server.tool("publish_kv", "Publish a key/value entry to BrewPage. Creates a KV s
614
754
  content: [
615
755
  {
616
756
  type: "text",
617
- text: formatPublishResponse(data),
757
+ text: formatPublishResponse(data, effectiveNamespace),
618
758
  },
619
759
  ],
620
760
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brewpage-mcp",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "mcpName": "io.github.kochetkov-ma/brewpage-mcp",
5
5
  "description": "MCP server for BrewPage — publish and manage HTML, KV, JSON, and file content from AI assistants",
6
6
  "type": "module",