Skip to content

v4.2.0 — read block content from a file, don't retype it

Choose a tag to compare

@patrickdeanfox patrickdeanfox released this 16 Jul 18:23
· 20 commits to main since this release

Install

Claude Desktop (one-click): download zuar-portal-mcp.mcpb below and double-click it.

Claude Code: point your client at this repo's dist/index.js for now — the npm path (npx -y zuar-portal-mcp-server) is prepared but not yet published, so the docs describe it ahead of it being live. See the note at the bottom.


The MCP made you retype a block to change one line of it

Block content could only arrive as inline tool-call arguments. Changing one line of an ~85k-character block meant a model regenerating all of it, token by token.

That isn't a copy, it's a re-transcription — and it fails in the worst way available. An em dash inside a regex character class ([—–-]) arriving as a hyphen changes what the pattern matches, renders fine, and passes review.

validate_block didn't close it either: you'd validate one transcription and then write a second, independently generated one. Nothing bound the two together, so a clean validation was never evidence about the bytes that landed.

The real cost wasn't tokens. Careful operators routed around the server to do the right thing — pulling the block down, editing it on disk, pushing it back with a hand-rolled script that necessarily skipped the authoring rules, the structural normalizer, the referential gate, and the VC commit. A safety layer that can't do the job gets bypassed, and then none of it applies.

Now: pass a path

update_block { block_id: "…", html_file: "./blocks/foo/index.html" }

create_block / update_block / validate_block accept html_file and css_file. The server reads the bytes (html_filejson_data.html, css_filecss), so a model never carries them. Every gate runs identically whichever door content came through. A file and its inline twin is an error, never a silent precedence.

Two guards, because the door would otherwise be a hole:

  • Containment (PORTAL_FILE_ROOTS; defaults CWD + VC dir), checked twice — once lexically before any filesystem access (otherwise "no such file" vs "outside the roots" is a file-existence oracle), then again on the realpath, since a symlink inside a root can point anywhere.
  • Size — the boundary guard weighs the arguments, so a 40-byte path would smuggle an unbounded file past PORTAL_MAX_INPUT_BYTES. The cap is re-applied to the file, stat'd before the read.

14 new tests assert the outgoing request body equals the file on disk byte for byte, with the actual hazard as the fixture.

Install & onboarding overhaul

The docs told a new user to clone and build a TypeScript project. Worse, the README's own install example put credentials in the client's env map — teaching the exact cross-portal hazard binding exists to catch: one global portal every folder inherits, so a project you believe is staging publishes to production. Credentials now belong to the project.

Two things that blocked a new user outright are fixed:

  • The binding wall is documented. Default PORTAL_BINDING_MODE=project means an unbound folder reads fine and has every write refused. Anyone following the old README hit that with no hint in it.
  • Both README install links were dead (#-install--… vs the rendered #install--…) — the two links a new user is most likely to click. CI now verifies every link and anchor.

Also: docs no longer tell users to edit files inside the installed package (those live in npm's cache or the .mcpb and are wiped on upgrade); docs/ is user-only, with build/release mechanics moved to CONTRIBUTING.md; badges come from npm so they can't drift like the 4.1.1 version literal did.

Upgrading

Install this .mcpb over your existing one, then restart Claude Desktop — a running client keeps the process it started, so new tools won't appear until it restarts.

npm is not live yet. Publishing is fully wired (.github/workflows/release.yml publishes with provenance and refuses a tag that disagrees with package.json) but needs an NPM_TOKEN secret. Until then npx -y zuar-portal-mcp-server will not resolve, and the README's npm badge renders as "not found".

Full changelog: CHANGELOG.md