brewpage-mcp 1.14.4 → 1.15.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 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.4
20
+ npx -y brewpage-mcp@1.15.0
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.4"]
36
+ "args": ["-y", "brewpage-mcp@1.15.0"]
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.4
47
+ claude mcp add --transport stdio --scope project brewpage -- npx -y brewpage-mcp@1.15.0
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.4"]
61
+ "args": ["-y", "brewpage-mcp@1.15.0"]
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.4"]
75
+ "args": ["-y", "brewpage-mcp@1.15.0"]
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.4
83
+ npm install -g brewpage-mcp@1.15.0
84
84
  brewpage-mcp
85
85
  ```
86
86
 
@@ -215,7 +215,7 @@ publish_file(url="https://example.com/diagram.png")
215
215
 
216
216
  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.
217
217
 
218
- Limits (current values from `get_limits`): a request is at most 25 MB; a site at most 200 files, 5 MB per file and 20 MB in total; each path at most 380 characters. System files such as `.DS_Store` and `__MACOSX/` are skipped and never count; files with an unsupported extension or a too-long path are skipped and reported in notices. Entry: `entry`, else root `index.html`, else the shallowest HTML file. A site limit hit answers 400 with a message naming the limit; 413 means the request is over 25 MB, has too many files and form fields, or a file name/path is too long; 422 means no servable file is left.
218
+ Limits (current values from `get_limits`): a request is at most 25 MB; a site at most 200 files, 5 MB per file and 20 MB in total; each path at most 380 characters. System files such as `.DS_Store` and `__MACOSX/` are skipped and never count; files with an unsupported extension or a too-long path are skipped and reported in notices. Entry: `entry`, else root `index.html`, else the shallowest HTML file. A site limit hit answers 400 with a message naming the limit, as does a PDF/Office file that is unreadable, password-protected or too complex; 413 means the request is over 25 MB, has too many files and form fields, or a file name/path is too long; 422 means no servable file is left; 503 means busy: retry after `Retry-After` seconds (failed publish tools print it).
219
219
 
220
220
  Parameters: exactly one of `entryContent` (string) or non-empty `files` (array of `{path, content}`), `entry` (optional entry file path, default `index.html`), `namespace` (optional -- omit for private, unlisted content; `public` lists the site in the gallery and sitemap for Google, Bing and AI search; a site always gets its own subdomain), `password` (optional), `ttlDays` (days or a string such as `2w`; default 15; the server clamps to 1--30), `ownerToken` (optional, groups site under an existing owner), `deliveryMode` (optional; fresh sites require `subdomain`).
221
221
 
@@ -513,6 +513,14 @@ Every publish response includes an **owner token** -- the only credential that a
513
513
 
514
514
  ## Changelog
515
515
 
516
+ ## 1.15.0 -- 2026-10-27
517
+
518
+ - Supports MCP protocol 2026-07-28 (SDK v2).
519
+
520
+ ## 1.14.5 -- 2026-10-27
521
+
522
+ - Site publish hints add 400 for an unreadable, password-protected or too complex PDF/Office file and 503 when busy; a failed publish on 503 prints `Retry-After`.
523
+
516
524
  ## 1.14.4 -- 2026-10-26
517
525
 
518
526
  - Remote mode: a request keeps its slot until the tool finishes, and a client disconnect cancels its backend calls and source download; backend calls time out after 60 s. Separate pools for light, write and large calls queue briefly instead of refusing; request body up to 32 MiB. `BREWPAGE_SHARED_EGRESS_CIDRS` and `BREWPAGE_SHARED_EGRESS_LIST_URLS` exempt platform egress from per-IP caps. Tools are unchanged.
package/dist/http.js CHANGED
@@ -5,9 +5,8 @@ import { lookup as dnsLookup } from "node:dns";
5
5
  import { BlockList, isIP } from "node:net";
6
6
  import { realpathSync } from "node:fs";
7
7
  import { pathToFileURL } from "node:url";
8
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
10
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
+ import { createMcpHandler, InMemoryTransport, isLegacyRequest, LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/server";
9
+ import { NodeStreamableHTTPServerTransport, toWebRequest } from "@modelcontextprotocol/node";
11
10
  import { createServer, OWNER_TOKEN_ENV, USER_AGENT } from "./tools.js";
12
11
  const MIB = 1024 * 1024;
13
12
  /** Fits the largest site (25 MiB backend request: 20 MiB of inline text plus JSON escaping).
@@ -54,6 +53,7 @@ const VERSION = USER_AGENT.split("/")[1];
54
53
  const REMOTE_USER_AGENT = `${USER_AGENT} (remote)`;
55
54
  const PARSE_ERROR = -32700;
56
55
  const INVALID_REQUEST = -32600;
56
+ const METHOD_NOT_FOUND = -32601;
57
57
  const SERVER_ERROR = -32000;
58
58
  /** One JSON line per event on stdout; callers never pass arguments, tokens or IPs. */
59
59
  function log(fields) {
@@ -342,18 +342,40 @@ function readBody(req, limit, onLarge) {
342
342
  finish("aborted");
343
343
  });
344
344
  }
345
- /** Tool names from an in-memory handshake; logs name only these. */
345
+ /** Tool names from an in-memory 2025 handshake (raw JSON-RPC, no client package); logs name only these. */
346
346
  async function registeredToolNames() {
347
347
  const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
348
348
  const server = createServer({ baseUrl: "http://127.0.0.1:9" });
349
- const client = new Client({ name: "startup", version: VERSION });
349
+ const answers = new Map();
350
+ clientSide.onmessage = (message) => {
351
+ if ("id" in message && typeof message.id === "number")
352
+ answers.get(message.id)?.(message);
353
+ };
354
+ const call = (id, method, params) => new Promise((resolve) => {
355
+ answers.set(id, resolve);
356
+ void clientSide.send({ jsonrpc: "2.0", id, method, params });
357
+ });
350
358
  await server.connect(serverSide);
351
- await client.connect(clientSide);
352
- const { tools } = await client.listTools();
353
- await client.close();
359
+ await clientSide.start();
360
+ await call(1, "initialize", { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "startup", version: VERSION } });
361
+ await clientSide.send({ jsonrpc: "2.0", method: "notifications/initialized" });
362
+ const listed = await call(2, "tools/list", {});
363
+ await clientSide.close();
354
364
  await server.close();
365
+ const tools = ("result" in listed ? listed.result.tools : undefined) ?? [];
366
+ if (tools.length === 0)
367
+ throw new Error("tools/list returned no tools");
355
368
  return new Set(tools.map(({ name }) => name));
356
369
  }
370
+ /** Tool-level outcome of one JSON-RPC answer; undefined = success. */
371
+ function rpcOutcome(outgoing) {
372
+ if (outgoing === null || typeof outgoing !== "object")
373
+ return undefined;
374
+ if ("error" in outgoing)
375
+ return "rpc_error";
376
+ const result = outgoing.result;
377
+ return result?.isError === true ? "tool_error" : undefined;
378
+ }
357
379
  /** Slot pool: global limit with a FIFO wait queue, plus a per-client cap (client undefined = shared egress, no cap). */
358
380
  class Pool {
359
381
  limit;
@@ -540,6 +562,15 @@ async function main() {
540
562
  const toolName = rpc.method === "tools/call" ? rpc.params?.name : undefined;
541
563
  entry.method = typeof rpc.method === "string" ? rpc.method.slice(0, MAX_LOGGED_METHOD) : null;
542
564
  entry.tool = rpc.method === "tools/call" ? (typeof toolName === "string" && toolNames.has(toolName) ? toolName : "unknown") : null;
565
+ // Stateless: no server-push streams, as the 2025 GET stream (405); a refused auto-open never fails a connect.
566
+ if (rpc.method === "subscriptions/listen") {
567
+ const id = message.id;
568
+ res.writeHead(200, { "Content-Type": "application/json" });
569
+ res.end(JSON.stringify({ jsonrpc: "2.0", id: typeof id === "string" || typeof id === "number" ? id : null,
570
+ error: { code: METHOD_NOT_FOUND, message: "subscriptions/listen is not supported by this stateless endpoint." } }));
571
+ entry.outcome = "rpc_error";
572
+ return;
573
+ }
543
574
  const isLight = rpc.method !== "tools/call" || (typeof toolName === "string" && LIGHT_TOOLS.has(toolName));
544
575
  if (toolName === "publish_file" && slot !== heavy && !(await acquire(heavy)))
545
576
  return busy();
@@ -559,13 +590,34 @@ async function main() {
559
590
  : Promise.reject(new Error("Download limit reached for this address; try again in a few minutes.")),
560
591
  fetch: callBackend,
561
592
  });
562
- const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
593
+ const request = await toWebRequest(req, message, { signal: abort.signal });
594
+ if (!(await isLegacyRequest(request, message))) {
595
+ // 2026-07-28 envelope: one exchange through the SDK entry; any SSE body is read to its end.
596
+ const modern = createMcpHandler(() => server, { legacy: "reject" });
597
+ try {
598
+ const response = await modern.fetch(request, { parsedBody: message });
599
+ const text = await response.text();
600
+ if ((response.headers.get("content-type") ?? "").startsWith("application/json")) {
601
+ try {
602
+ entry.outcome = rpcOutcome(JSON.parse(text)) ?? null;
603
+ }
604
+ catch {
605
+ entry.outcome = null;
606
+ }
607
+ }
608
+ res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
609
+ res.end(text);
610
+ }
611
+ finally {
612
+ await modern.close();
613
+ await server.close();
614
+ }
615
+ return;
616
+ }
617
+ const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
563
618
  const send = transport.send.bind(transport);
564
619
  transport.send = (outgoing, options) => {
565
- if ("error" in outgoing)
566
- entry.outcome = "rpc_error";
567
- else if ("result" in outgoing && outgoing.result.isError === true)
568
- entry.outcome = "tool_error";
620
+ entry.outcome = rpcOutcome(outgoing) ?? entry.outcome;
569
621
  return send(outgoing, options);
570
622
  };
571
623
  try {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
3
  import { createServer, DEFAULT_BASE_URL, OWNER_TOKEN_ENV } from "./tools.js";
4
4
  const OWNER_TOKEN_SHAPE = /^[A-Za-z0-9]{32}$/;
5
5
  /** Optional default owner token for every call; a malformed value is ignored with one stderr line. */
@@ -12,12 +12,5 @@ const ENV_OWNER_TOKEN = (() => {
12
12
  console.error(`${OWNER_TOKEN_ENV} ignored: expected 32 letters/digits.`);
13
13
  return undefined;
14
14
  })();
15
- async function main() {
16
- const server = createServer({ baseUrl: process.env.BREWPAGE_URL || DEFAULT_BASE_URL, ownerToken: ENV_OWNER_TOKEN });
17
- const transport = new StdioServerTransport();
18
- await server.connect(transport);
19
- }
20
- main().catch((error) => {
21
- console.error("Fatal error:", error);
22
- process.exit(1);
23
- });
15
+ /** Serves 2026-07-28 (server/discover) and 2025-era initialize openings from one factory. */
16
+ serveStdio(() => createServer({ baseUrl: process.env.BREWPAGE_URL || DEFAULT_BASE_URL, ownerToken: ENV_OWNER_TOKEN }));
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  export declare const DEFAULT_BASE_URL = "https://brewpage.app";
3
- export declare const USER_AGENT = "brewpage-mcp/1.14.4";
3
+ export declare const USER_AGENT = "brewpage-mcp/1.15.0";
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. */
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
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.4";
4
+ export const USER_AGENT = "brewpage-mcp/1.15.0";
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 };
@@ -46,9 +46,14 @@ function maskToken(token) {
46
46
  function textResult(text, isError = false) {
47
47
  return isError ? { content: [{ type: "text", text }], isError: true } : { content: [{ type: "text", text }] };
48
48
  }
49
+ /** ` Retry-After: <value>.` for a busy 503 that sends the header, else empty. */
50
+ function retryAfterText(status, headers) {
51
+ const after = status === 503 ? headers?.get("retry-after") : null;
52
+ return after ? ` Retry-After: ${after}.` : "";
53
+ }
49
54
  /** Owner-tool failure: the server answer verbatim plus a hint for a plain wrong-token 403 or a frozen old publication. */
50
- function ownerFailure(operation, status, data, fromEnv) {
51
- const lines = [`Failed to ${operation} (${status}): ${JSON.stringify(data)}`];
55
+ function ownerFailure(operation, status, data, fromEnv, headers) {
56
+ const lines = [`Failed to ${operation} (${status}): ${JSON.stringify(data)}${retryAfterText(status, headers)}`];
52
57
  const code = data?.code;
53
58
  if (status === 403 && (code === undefined || code === "OWNER_AUTHORIZATION_FAILED")) {
54
59
  lines.push(`Wrong owner token for this publication. Use ownerToken from create.${fromEnv ? ` Sent: ${OWNER_TOKEN_ENV}.` : ""}`);
@@ -106,8 +111,9 @@ const DELIVERY_MODE_SCHEMA = z.enum(["path", "subdomain"]).optional().describe("
106
111
  const SITE_LIMITS_TEXT = "Default limits (current values: GET /api/limits): a request is at most 25 MB; a site at most 200 files, 5 MB per file and 20 MB in total; " +
107
112
  "each path at most 380 characters. System files (.DS_Store, __MACOSX/ and similar) are skipped and never count; files with an unsupported extension " +
108
113
  "or a too-long path are skipped and listed in notices. Entry: `entry`, else root index.html, else the shallowest HTML file. " +
109
- "Errors: 400 = a site limit exceeded (the message names it), invalid path or no HTML file; 413 = request over 25 MB, " +
110
- "too many files and form fields, or a file name/path too long; 422 = no servable file left.";
114
+ "Errors: 400 = a site limit exceeded (the message names it), invalid path, no HTML file, or a PDF/Office file unreadable, " +
115
+ "password-protected or too complex; 413 = request over 25 MB, too many files and form fields, or a file name/path too long; " +
116
+ "422 = no servable file left; 503 = busy, retry after Retry-After seconds.";
111
117
  function hostingResponseLines(data) {
112
118
  const fields = ["routingCohort", "deliveryMode", "requestedDeliveryMode", "effectiveDeliveryMode",
113
119
  "deliveryModeMatched", "modeLocked", "hostingVersion", "access", "managementLink"];
@@ -182,7 +188,7 @@ export function createServer(options = {}) {
182
188
  const apiFetch = options.fetch ?? fetch;
183
189
  const server = new McpServer({
184
190
  name: "brewpage-mcp",
185
- version: "1.14.4",
191
+ version: "1.15.0",
186
192
  });
187
193
  /** Caller argument first, then the configured default; `fromEnv` marks the configured one. */
188
194
  function ownerTokenFor(argument) {
@@ -300,13 +306,13 @@ export function createServer(options = {}) {
300
306
  const owner = ownerTokenFor(ownerToken).token;
301
307
  if (owner)
302
308
  headers["X-Owner-Token"] = owner;
303
- const { ok, data } = await apiRequest("POST", path, body, headers);
309
+ const { ok, status, data, headers: answerHeaders } = await apiRequest("POST", path, body, headers);
304
310
  if (!ok) {
305
311
  return {
306
312
  content: [
307
313
  {
308
314
  type: "text",
309
- text: `Failed to publish: ${JSON.stringify(data)}`,
315
+ text: `Failed to publish: ${JSON.stringify(data)}${retryAfterText(status, answerHeaders)}`,
310
316
  },
311
317
  ],
312
318
  isError: true,
@@ -357,7 +363,7 @@ export function createServer(options = {}) {
357
363
  content: [
358
364
  {
359
365
  type: "text",
360
- text: `Failed to upload file: ${JSON.stringify(data)}`,
366
+ text: `Failed to upload file: ${JSON.stringify(data)}${retryAfterText(res.status, res.headers)}`,
361
367
  },
362
368
  ],
363
369
  isError: true,
@@ -577,7 +583,7 @@ export function createServer(options = {}) {
577
583
  content: [
578
584
  {
579
585
  type: "text",
580
- text: `Failed to publish site: ${JSON.stringify(data)}`,
586
+ text: `Failed to publish site: ${JSON.stringify(data)}${retryAfterText(res.status, res.headers)}`,
581
587
  },
582
588
  ],
583
589
  isError: true,
@@ -673,7 +679,7 @@ export function createServer(options = {}) {
673
679
  const res = await bpFetch(url, { method: "PUT", headers, body: formData });
674
680
  const data = await res.json().catch(() => null);
675
681
  if (!res.ok)
676
- return ownerFailure("republish site", res.status, data, auth.fromEnv);
682
+ return ownerFailure("republish site", res.status, data, auth.fromEnv, res.headers);
677
683
  const site = data;
678
684
  const lines = [
679
685
  `Site republished successfully (full replace)!`,
@@ -744,7 +750,7 @@ export function createServer(options = {}) {
744
750
  inputSchema: {
745
751
  deliveryMode: DELIVERY_MODE_SCHEMA,
746
752
  json: z
747
- .union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
753
+ .union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())])
748
754
  .describe("JSON document to publish (object, array, or JSON-encoded string)"),
749
755
  namespace: z
750
756
  .string()
@@ -843,7 +849,7 @@ export function createServer(options = {}) {
843
849
  namespace: z.string().describe("Document namespace"),
844
850
  id: z.string().describe("Document ID"),
845
851
  json: z
846
- .union([z.string(), z.record(z.unknown()), z.array(z.unknown())])
852
+ .union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())])
847
853
  .describe("New JSON document (object, array, or JSON-encoded string)"),
848
854
  ownerToken: z.string().optional().describe(OWNER_TOKEN_DESCRIPTION),
849
855
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brewpage-mcp",
3
- "version": "1.14.4",
3
+ "version": "1.15.0",
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",
@@ -22,7 +22,7 @@
22
22
  "start": "node dist/index.js",
23
23
  "start:http": "node dist/http.js",
24
24
  "dev": "tsx src/index.ts",
25
- "test": "npm run build && node --import tsx --test test/smoke.test.ts test/http.test.ts",
25
+ "test": "npm run build && node --import tsx --test test/smoke.test.ts test/http.test.ts test/protocol.test.ts",
26
26
  "prepublishOnly": "npm run build"
27
27
  },
28
28
  "keywords": [
@@ -54,21 +54,18 @@
54
54
  "url": "https://github.com/kochetkov-ma/brewpage-openapi/issues"
55
55
  },
56
56
  "dependencies": {
57
- "@modelcontextprotocol/sdk": "1.29.0",
58
- "zod": "3.25.76"
57
+ "@modelcontextprotocol/node": "2.1.2",
58
+ "@modelcontextprotocol/server": "2.4.0",
59
+ "zod": "4.6.7"
59
60
  },
60
61
  "devDependencies": {
62
+ "@modelcontextprotocol/client": "2.4.0",
61
63
  "@types/node": "22.19.15",
62
64
  "tsx": "4.22.0",
63
65
  "typescript": "5.9.3"
64
66
  },
65
67
  "overrides": {
66
68
  "@hono/node-server": "1.19.15",
67
- "body-parser": "2.3.0",
68
- "fast-uri": "3.1.8",
69
- "hono": "4.13.7",
70
- "ip-address": "10.7.1",
71
- "proxy-addr": "2.0.8",
72
- "qs": "6.16.0"
69
+ "hono": "4.13.14"
73
70
  }
74
71
  }