brewpage-mcp 1.14.0 → 1.14.2

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 CHANGED
@@ -17,7 +17,7 @@ Choosing a host? See how BrewPage compares with GitHub Pages, Netlify Drop, Tiin
17
17
  ## Quick Start
18
18
 
19
19
  ```bash
20
- npx -y brewpage-mcp@1.14.0
20
+ npx -y brewpage-mcp@1.14.2
21
21
  ```
22
22
 
23
23
  Requires Node.js 20 or newer. This starts the stdio server for an MCP client.
@@ -33,7 +33,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
33
33
  "mcpServers": {
34
34
  "brewpage": {
35
35
  "command": "npx",
36
- "args": ["-y", "brewpage-mcp@1.14.0"]
36
+ "args": ["-y", "brewpage-mcp@1.14.2"]
37
37
  }
38
38
  }
39
39
  }
@@ -44,7 +44,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
44
44
  Register a project-scoped stdio server:
45
45
 
46
46
  ```bash
47
- claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.14.0
47
+ claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.14.2
48
48
  ```
49
49
 
50
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).
@@ -58,7 +58,7 @@ Open **Settings > MCP** and add:
58
58
  "mcpServers": {
59
59
  "brewpage": {
60
60
  "command": "npx",
61
- "args": ["-y", "brewpage-mcp@1.14.0"]
61
+ "args": ["-y", "brewpage-mcp@1.14.2"]
62
62
  }
63
63
  }
64
64
  }
@@ -72,7 +72,7 @@ Open the Cline MCP settings panel and add:
72
72
  {
73
73
  "brewpage": {
74
74
  "command": "npx",
75
- "args": ["-y", "brewpage-mcp@1.14.0"]
75
+ "args": ["-y", "brewpage-mcp@1.14.2"]
76
76
  }
77
77
  }
78
78
  ```
@@ -80,7 +80,7 @@ Open the Cline MCP settings panel and add:
80
80
  ### Global Install
81
81
 
82
82
  ```bash
83
- npm install -g brewpage-mcp@1.14.0
83
+ npm install -g brewpage-mcp@1.14.2
84
84
  brewpage-mcp
85
85
  ```
86
86
 
@@ -509,6 +509,14 @@ Every publish response includes an **owner token** -- the only credential that a
509
509
 
510
510
  ## Changelog
511
511
 
512
+ ## 1.14.2 -- 2026-10-26
513
+
514
+ - MCP Registry entry lists the remote endpoint `https://brewpage.app/mcp` (Streamable HTTP, no auth) beside the npm package. Tools are unchanged.
515
+
516
+ ## 1.14.1 -- 2026-10-26
517
+
518
+ - Security fix: `namespace`, `id` and `key` are encoded as single path segments; empty, `.` and `..` are refused before any request, so calls stay under `/api/<type>/`. Valid inputs are unchanged.
519
+
512
520
  ## 1.14.0 -- 2026-10-26
513
521
 
514
522
  - Remote mode: `node dist/http.js` serves stateless Streamable HTTP (see [Remote HTTP mode](#remote-http-mode)); stdio is unchanged. `User-Agent: brewpage-mcp/1.14.0` (`(remote)` in HTTP mode).
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  export declare const DEFAULT_BASE_URL = "https://brewpage.app";
3
- export declare const USER_AGENT = "brewpage-mcp/1.14.0";
3
+ export declare const USER_AGENT = "brewpage-mcp/1.14.2";
4
4
  export declare const OWNER_TOKEN_ENV = "BREWPAGE_OWNER_TOKEN";
5
5
  export interface ServerOptions {
6
6
  /** BrewPage origin for API calls; default https://brewpage.app. */
@@ -18,5 +18,9 @@ export interface ServerOptions {
18
18
  /** Transport for BrewPage API calls; default global fetch. */
19
19
  fetch?: typeof fetch;
20
20
  }
21
+ /** `/api/<segments>` with each segment encoded; empty, `.` and `..` refused (URL parsing would resolve them away). */
22
+ export declare function apiPath(...segments: string[]): string;
23
+ /** Path plus URLSearchParams query; undefined values skipped, no `?` when empty. */
24
+ export declare function withQuery(path: string, params: Array<[string, string | number | undefined]>): string;
21
25
  /** Fresh server with all tools; stdio builds one, stateless HTTP one per request. */
22
26
  export declare function createServer(options?: ServerOptions): McpServer;
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  export const DEFAULT_BASE_URL = "https://brewpage.app";
4
- export const USER_AGENT = "brewpage-mcp/1.14.0";
4
+ export const USER_AGENT = "brewpage-mcp/1.14.2";
5
5
  export const OWNER_TOKEN_ENV = "BREWPAGE_OWNER_TOKEN";
6
6
  const READ_ONLY = { readOnlyHint: true };
7
7
  const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
@@ -21,6 +21,24 @@ const OLD_READ_ONLY_TEXT = ` Old publications (made before 5 October 2026) are r
21
21
  const WARNING_CODES = new Set(["secret-detected", "manage-link-disabled"]);
22
22
  /** MCP type name -> REST path segment. */
23
23
  const TYPE_SEGMENTS = { html: "html", site: "sites", file: "files", json: "json", kv: "kv" };
24
+ /** `/api/<segments>` with each segment encoded; empty, `.` and `..` refused (URL parsing would resolve them away). */
25
+ export function apiPath(...segments) {
26
+ for (const segment of segments) {
27
+ if (segment === "" || segment === "." || segment === "..") {
28
+ throw new Error(`Invalid path value ${JSON.stringify(segment)}: empty, "." and ".." are not allowed.`);
29
+ }
30
+ }
31
+ return `/api/${segments.map(encodeURIComponent).join("/")}`;
32
+ }
33
+ /** Path plus URLSearchParams query; undefined values skipped, no `?` when empty. */
34
+ export function withQuery(path, params) {
35
+ const query = new URLSearchParams();
36
+ for (const [name, value] of params)
37
+ if (value !== undefined)
38
+ query.append(name, String(value));
39
+ const qs = query.toString();
40
+ return qs ? `${path}?${qs}` : path;
41
+ }
24
42
  /** Only the last 4 characters: tokens never reach a transcript in full. */
25
43
  function maskToken(token) {
26
44
  return `…${token.slice(-4)}`;
@@ -162,7 +180,7 @@ export function createServer(options = {}) {
162
180
  const apiFetch = options.fetch ?? fetch;
163
181
  const server = new McpServer({
164
182
  name: "brewpage-mcp",
165
- version: "1.14.0",
183
+ version: "1.14.2",
166
184
  });
167
185
  /** Caller argument first, then the configured default; `fromEnv` marks the configured one. */
168
186
  function ownerTokenFor(argument) {
@@ -266,9 +284,7 @@ export function createServer(options = {}) {
266
284
  },
267
285
  }, async ({ content, format, namespace, password, ttlDays, filename, showTopBar, deliveryMode, ownerToken }) => {
268
286
  const effectiveNamespace = resolveNamespace(namespace);
269
- const qsParams = [`ns=${encodeURIComponent(effectiveNamespace)}`, `format=${encodeURIComponent(format.toLowerCase())}`];
270
- if (ttlDays !== undefined)
271
- qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
287
+ const path = withQuery(apiPath("html"), [["ns", effectiveNamespace], ["format", format.toLowerCase()], ["ttl", ttlDays]]);
272
288
  const body = { content };
273
289
  if (filename)
274
290
  body.filename = filename;
@@ -282,7 +298,7 @@ export function createServer(options = {}) {
282
298
  const owner = ownerTokenFor(ownerToken).token;
283
299
  if (owner)
284
300
  headers["X-Owner-Token"] = owner;
285
- const { ok, data } = await apiRequest("POST", `/api/html?${qsParams.join("&")}`, body, headers);
301
+ const { ok, data } = await apiRequest("POST", path, body, headers);
286
302
  if (!ok) {
287
303
  return {
288
304
  content: [
@@ -328,7 +344,7 @@ export function createServer(options = {}) {
328
344
  const owner = ownerTokenFor(ownerToken).token;
329
345
  if (owner)
330
346
  headers["X-Owner-Token"] = owner;
331
- const res = await bpFetch(`${baseUrl}/api/files?ns=${encodeURIComponent(effectiveNamespace)}`, {
347
+ const res = await bpFetch(`${baseUrl}${withQuery(apiPath("files"), [["ns", effectiveNamespace]])}`, {
332
348
  method: "POST",
333
349
  headers,
334
350
  body: formData,
@@ -369,7 +385,7 @@ export function createServer(options = {}) {
369
385
  const auth = ownerTokenFor(ownerToken);
370
386
  if (!auth.token)
371
387
  return textResult(NO_OWNER_TOKEN, true);
372
- const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/hosting`;
388
+ const path = apiPath(TYPE_SEGMENTS[type], namespace, id, "hosting");
373
389
  const { ok, status, data } = await apiRequest("PATCH", path, { deliveryMode, expectedVersion }, { "X-Owner-Token": auth.token });
374
390
  if (!ok)
375
391
  return ownerFailure("update hosting", status, data, auth.fromEnv);
@@ -392,7 +408,7 @@ export function createServer(options = {}) {
392
408
  const auth = ownerTokenFor(ownerToken);
393
409
  if (!auth.token)
394
410
  return textResult(NO_OWNER_TOKEN, true);
395
- const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
411
+ const path = apiPath(TYPE_SEGMENTS[type], namespace, id);
396
412
  const { ok, status, data } = await apiRequest("DELETE", path, undefined, { "X-Owner-Token": auth.token });
397
413
  if (!ok)
398
414
  return ownerFailure("delete", status, data, auth.fromEnv);
@@ -420,7 +436,7 @@ export function createServer(options = {}) {
420
436
  const headers = {};
421
437
  if (password)
422
438
  headers["X-Password"] = password;
423
- const { ok, status, data, headers: responseHeaders } = await apiRequest("GET", `/api/html/${namespace}/${id}`, undefined, headers, "text");
439
+ const { ok, status, data, headers: responseHeaders } = await apiRequest("GET", apiPath("html", namespace, id), undefined, headers, "text");
424
440
  if (!ok) {
425
441
  let errorData = data;
426
442
  try {
@@ -464,7 +480,7 @@ export function createServer(options = {}) {
464
480
  .describe("Optional IANA timezone id for the 'today' boundary, e.g. 'Europe/Lisbon'. Defaults to UTC. Invalid value falls back to UTC silently."),
465
481
  },
466
482
  }, async ({ tz }) => {
467
- const path = tz ? `/api/stats?tz=${encodeURIComponent(tz)}` : "/api/stats";
483
+ const path = withQuery(apiPath("stats"), [["tz", tz || undefined]]);
468
484
  const { ok, data } = await apiRequest("GET", path);
469
485
  if (!ok) {
470
486
  return {
@@ -551,14 +567,7 @@ export function createServer(options = {}) {
551
567
  if (deliveryMode)
552
568
  headers["X-Delivery-Mode"] = deliveryMode;
553
569
  const effectiveNamespace = resolveNamespace(namespace);
554
- const qsParams = [];
555
- if (entry)
556
- qsParams.push(`entry=${encodeURIComponent(entry)}`);
557
- qsParams.push(`ns=${encodeURIComponent(effectiveNamespace)}`);
558
- if (ttlDays)
559
- qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
560
- const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
561
- const url = `${baseUrl}/api/sites${qs}`;
570
+ const url = baseUrl + withQuery(apiPath("sites"), [["entry", entry || undefined], ["ns", effectiveNamespace], ["ttl", ttlDays || undefined]]);
562
571
  const res = await bpFetch(url, { method: "POST", headers, body: formData });
563
572
  const data = await res.json().catch(() => null);
564
573
  if (!res.ok) {
@@ -657,15 +666,8 @@ export function createServer(options = {}) {
657
666
  if (!auth.token)
658
667
  return textResult(NO_OWNER_TOKEN, true);
659
668
  const headers = { "X-Owner-Token": auth.token };
660
- const qsParams = [];
661
- if (entry)
662
- qsParams.push(`entry=${encodeURIComponent(entry)}`);
663
- if (ttlDays)
664
- qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
665
- if (tags && tags.length)
666
- qsParams.push(`tags=${encodeURIComponent(tags.join(","))}`);
667
- const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
668
- const url = `${baseUrl}/api/sites/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}${qs}`;
669
+ const url = baseUrl + withQuery(apiPath("sites", namespace, id), [["entry", entry || undefined],
670
+ ["ttl", ttlDays || undefined], ["tags", tags && tags.length ? tags.join(",") : undefined]]);
669
671
  const res = await bpFetch(url, { method: "PUT", headers, body: formData });
670
672
  const data = await res.json().catch(() => null);
671
673
  if (!res.ok)
@@ -718,7 +720,7 @@ export function createServer(options = {}) {
718
720
  const auth = ownerTokenFor(ownerToken);
719
721
  if (!auth.token)
720
722
  return textResult(NO_OWNER_TOKEN, true);
721
- const path = `/api/html/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
723
+ const path = apiPath("html", namespace, id);
722
724
  const { ok, status, data } = await apiRequest("PUT", path, body, {
723
725
  "X-Owner-Token": auth.token,
724
726
  });
@@ -760,15 +762,7 @@ export function createServer(options = {}) {
760
762
  }, async ({ json, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
761
763
  const jsonString = typeof json === "string" ? json : JSON.stringify(json);
762
764
  const effectiveNamespace = resolveNamespace(namespace);
763
- const qsParams = [];
764
- qsParams.push(`ns=${encodeURIComponent(effectiveNamespace)}`);
765
- if (ttlDays !== undefined)
766
- qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
767
- if (tags && tags.length) {
768
- for (const t of tags)
769
- qsParams.push(`tags=${encodeURIComponent(t)}`);
770
- }
771
- const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
765
+ const path = withQuery(apiPath("json"), [["ns", effectiveNamespace], ["ttl", ttlDays], ...(tags ?? []).map((t) => ["tags", t])]);
772
766
  const headers = { "Content-Type": "application/json" };
773
767
  if (password)
774
768
  headers["X-Password"] = password;
@@ -777,7 +771,7 @@ export function createServer(options = {}) {
777
771
  headers["X-Owner-Token"] = owner;
778
772
  if (deliveryMode)
779
773
  headers["X-Delivery-Mode"] = deliveryMode;
780
- const res = await bpFetch(`${baseUrl}/api/json${qs}`, {
774
+ const res = await bpFetch(`${baseUrl}${path}`, {
781
775
  method: "POST",
782
776
  headers,
783
777
  body: jsonString,
@@ -818,7 +812,7 @@ export function createServer(options = {}) {
818
812
  const headers = {};
819
813
  if (password)
820
814
  headers["X-Password"] = password;
821
- const path = `/api/json/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
815
+ const path = apiPath("json", namespace, id);
822
816
  const { ok, status, data } = await apiRequest("GET", path, undefined, headers);
823
817
  if (!ok) {
824
818
  return {
@@ -856,7 +850,7 @@ export function createServer(options = {}) {
856
850
  const auth = ownerTokenFor(ownerToken);
857
851
  if (!auth.token)
858
852
  return textResult(NO_OWNER_TOKEN, true);
859
- const url = `${baseUrl}/api/json/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}`;
853
+ const url = baseUrl + apiPath("json", namespace, id);
860
854
  const res = await bpFetch(url, {
861
855
  method: "PUT",
862
856
  headers: {
@@ -903,15 +897,7 @@ export function createServer(options = {}) {
903
897
  }, async ({ key, value, namespace, password, ttlDays, tags, ownerToken, deliveryMode }) => {
904
898
  const body = { key, value };
905
899
  const effectiveNamespace = resolveNamespace(namespace);
906
- const qsParams = [];
907
- qsParams.push(`ns=${encodeURIComponent(effectiveNamespace)}`);
908
- if (ttlDays !== undefined)
909
- qsParams.push(`ttl=${encodeURIComponent(String(ttlDays))}`);
910
- if (tags && tags.length) {
911
- for (const t of tags)
912
- qsParams.push(`tags=${encodeURIComponent(t)}`);
913
- }
914
- const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
900
+ const path = withQuery(apiPath("kv"), [["ns", effectiveNamespace], ["ttl", ttlDays], ...(tags ?? []).map((t) => ["tags", t])]);
915
901
  const headers = {};
916
902
  if (password)
917
903
  headers["X-Password"] = password;
@@ -920,7 +906,7 @@ export function createServer(options = {}) {
920
906
  headers["X-Owner-Token"] = owner;
921
907
  if (deliveryMode)
922
908
  headers["X-Delivery-Mode"] = deliveryMode;
923
- const { ok, status, data } = await apiRequest("POST", `/api/kv${qs}`, body, headers);
909
+ const { ok, status, data } = await apiRequest("POST", path, body, headers);
924
910
  if (!ok) {
925
911
  return {
926
912
  content: [
@@ -956,7 +942,7 @@ export function createServer(options = {}) {
956
942
  const auth = ownerTokenFor(ownerToken);
957
943
  if (!auth.token)
958
944
  return textResult(NO_OWNER_TOKEN, true);
959
- const path = `/api/kv/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/${encodeURIComponent(key)}`;
945
+ const path = apiPath("kv", namespace, id, key);
960
946
  const { ok, status, data } = await apiRequest("PUT", path, body, {
961
947
  "X-Owner-Token": auth.token,
962
948
  });
@@ -987,7 +973,7 @@ export function createServer(options = {}) {
987
973
  const headers = {};
988
974
  if (password)
989
975
  headers["X-Password"] = password;
990
- const path = `/api/kv/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/${encodeURIComponent(key)}`;
976
+ const path = apiPath("kv", namespace, id, key);
991
977
  const { ok, status, data } = await apiRequest("GET", path, undefined, headers);
992
978
  if (!ok) {
993
979
  return {
@@ -1073,22 +1059,12 @@ export function createServer(options = {}) {
1073
1059
  const auth = ownerTokenFor(ownerToken);
1074
1060
  if (mine === true && !auth.token)
1075
1061
  return textResult(NO_OWNER_TOKEN, true);
1076
- const qsParams = [];
1077
- if (q !== undefined)
1078
- qsParams.push(`q=${encodeURIComponent(q)}`);
1079
- if (page !== undefined)
1080
- qsParams.push(`page=${encodeURIComponent(String(page))}`);
1081
- if (size !== undefined)
1082
- qsParams.push(`size=${encodeURIComponent(String(size))}`);
1083
- if (sort)
1084
- qsParams.push(`sort=${encodeURIComponent(sort)}`);
1085
- if (mine === true)
1086
- qsParams.push("mine=true");
1087
- const qs = qsParams.length ? `?${qsParams.join("&")}` : "";
1062
+ const path = withQuery(apiPath("gallery"), [["q", q], ["page", page], ["size", size],
1063
+ ["sort", sort || undefined], ["mine", mine === true ? "true" : undefined]]);
1088
1064
  const headers = {};
1089
1065
  if (mine === true && auth.token)
1090
1066
  headers["X-Owner-Token"] = auth.token;
1091
- const { ok, status, data } = await apiRequest("GET", `/api/gallery${qs}`, undefined, headers);
1067
+ const { ok, status, data } = await apiRequest("GET", path, undefined, headers);
1092
1068
  if (!ok) {
1093
1069
  return {
1094
1070
  content: [
@@ -1123,7 +1099,7 @@ export function createServer(options = {}) {
1123
1099
  const auth = ownerTokenFor(ownerToken);
1124
1100
  if (!auth.token)
1125
1101
  return textResult(NO_OWNER_TOKEN, true);
1126
- const path = `/api/${TYPE_SEGMENTS[type]}/${encodeURIComponent(namespace)}/${encodeURIComponent(id)}/visibility`;
1102
+ const path = apiPath(TYPE_SEGMENTS[type], namespace, id, "visibility");
1127
1103
  const { ok, status, data } = await apiRequest("POST", path, { listed }, { "X-Owner-Token": auth.token });
1128
1104
  if (!ok)
1129
1105
  return ownerFailure("set visibility", status, data, auth.fromEnv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brewpage-mcp",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "mcpName": "io.github.kochetkov-ma/brewpage-mcp",
5
5
  "description": "MCP server for BrewPage — publish and manage HTML, sites, KV, JSON and files from AI assistants",
6
6
  "type": "module",