Releases: patrickdeanfox/zuar-portal-mcp
Release list
v4.3.0 — the hardening wave
The audit-driven production-hardening wave (#12–#21), in one release. Full narrative with every design decision: CHANGELOG 4.3.0.
Safety (the audit's HIGH/MED/LOW findings, all closed)
- SQL blast-radius classifier rewritten — burden of proof inverted, fails closed, checks at save time; CTE-led deletes,
WHERE 1=1, MERGE/ALTER/GRANT all caught - Duplicate-proof creates (ambiguous network failures verify-and-adopt, never blind-resend) and
expected_updated_atoptimistic concurrency on every update path html_filejail sealed (dotfiles + the credential file, symlinks included); credential file written 0600- Value-shape redaction (connection-string passwords, JWTs, PEM keys — in any value or name) applied to query/profile output; untrusted-content envelope on portal-authored data
- Binding re-verifies on a TTL + config reload; admin paths confirm-gated with self-lockout protection; rollback scope stated honestly
Token & performance defaults (each with an opt-out)
execute_query≤1,000 rows ·get_resource summary/fields(~2,750 → ~150 tok) · list byte-cap with auto-projection · compact JSON · 5s read cache (writes clear it) · server-side pagination on first pages (live-probed) · parallel snapshot fan-out
The autonomous-loop toolkit
cleanup_scratch+ theTMP ·naming scope ·score_portalwith a VC-committed regression baseline ·/portal-improve— snapshot → score → fix → adversary-verify → sweep → baseline only on proven non-regression
DX & docs
- ESLint in CI · generated tool reference (
npm run gen:docs, 40 tools, can't drift) · quickstart/concepts/FAQ tiers · broadened link checking ·server.tssplit begun (typed DI seam, behavior-frozen)
343 tests (was 276) · 40 tools · every PR CI-gated on Node 18/20/22.
🤖 Generated with Claude Code
v4.2.0 — read block content from a file, don't retype it
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_file → json_data.html, css_file → css), 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 therealpath, 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=projectmeans 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.ymlpublishes with provenance and refuses a tag that disagrees withpackage.json) but needs anNPM_TOKENsecret. Until thennpx -y zuar-portal-mcp-serverwill not resolve, and the README's npm badge renders as "not found".
Full changelog: CHANGELOG.md
v4.1.1 — confirm the portal in one call
Installing? Download
zuar-portal-mcp.mcpbbelow and double-click it. Claude Desktop asks for three portal values — no terminal, no config files.This is the first release since v1.0.0. Versions 2.x, 3.0.0 and 4.0.0 shipped to
mainbut were never tagged or released, so the previous "latest release" was three majors behind what the README documents. If you installed from a release before today, you were running v1.0.0 — this brings the one-click bundle up to current code.
A session opens in one call
Confirming a portal connection took three calls — get_capabilities (~4 KB of JSON describing the server, which never dials the portal), get_version, then a user/me read for the identity.
Worse: the one call three separate documents advertised as confirming connectivity could not fail. get_version swallowed both its requests to null, so a dead portal, an expired API key and a typo'd URL all reported success. Bad credentials only surfaced at the first write.
check_connection
One authenticated round-trip. ~230 characters against get_capabilities' ~3,950:
{ "connected": true, "portal": "analytics.acme.com", "url": "https://analytics.acme.com",
"version": "1.19.2", "signed_in_as": "pfox@acme.com", "binding": "single",
"writes": "content", "browser_assist": false }/auth/me is the probe because it forces the lazy login — so the credentials are genuinely exercised, not just echoed from config. It also returns the signed-in identity get_capabilities could never answer (it holds only the user UUID).
Failure returns the portal's real reason and an actionable fix:
{ "connected": false, "portal": "analytics.acme.com",
"reason": "Portal GET /auth/me failed: HTTP 401 - invalid api key",
"fix": "The portal rejected the credentials. Re-run configure_project (overwrite=true)..." }An unconfigured folder reports as setup (fix → configure_project), not as breakage.
zuar_portal_start prompt
The kickoff, surfaced as a slash command by MCP clients: call check_connection, report one line, ask what's next. Costs nothing until invoked.
Also in this release
get_versioncan fail again —/api/versionerrors are no longer swallowed./api/aboutstays best-effort, so a portal that doesn't serve it still returns a usable version.- One-call preflight everywhere —
check_connectionalso returnsurlandbrowser_assist, the other two reasons agents fetched the 4 KB posture dump. Every/portal-*command, all 10 specialist agents, the block pipeline and the generated projectCLAUDE.mdnow use the small call. - The server reports its real version —
SERVER_VERSIONwas a hardcoded literal stuck at3.0.0through the entire 4.0.0 release, so every client'sserverInfonamed a build that hadn't shipped in months. Now read frompackage.json. (The test that should have caught it asserted onlyversion.length > 0; it now pins topackage.json.) - Documentation drift, found by audit and fixed —
SECURITY.mdomitted an always-on tool that makes an authenticated call and returns the signed-in identity;docs/14documented the retired start sequence;docs/02told operators to verify an install with the call that couldn't fail; the agenttools:allowlists grantedget_versionbut notcheck_connection, which would have left the new call unreachable;portal-block-stylistclaimedget_capabilitiesreports theme tokens (it never has); the README version badge said3.0.0. block_perf_passwas registered but never advertised in the manifest — added, plus a prompt-parity test.
Upgrading
Drop-in from 4.0.0 — no breaking changes. Nothing is removed; get_capabilities remains for the full tool/group, VC and audit posture.
Full changelog: https://github.com/patrickdeanfox/zuar-portal-mcp/blob/main/CHANGELOG.md