zuar-portal-mcp
Enables natural language management of Zuar Portal instances, including authoring HTML blocks, building pages, managing data sources, queries, themes, and users, with versioned and revertible changes.
README
<div align="center">
Zuar Portal — MCP Server
Let Claude operate your Zuar Portal (zPortal) for you — author HTML blocks, build pages, manage data sources, queries, themes and users, explore real data, and keep a git-versioned, revertible history of every change, all through natural language.
</div>
An MCP server that exposes a Zuar Portal's REST + auth APIs to any MCP client (Claude Desktop, Claude Code, …). It turns "build me a sales dashboard" into the right sequence of authenticated calls — discover data sources → write a saved query → author a validated HTML block → bind it → place it on a page — with bundled authoring guidance, layered write-safety, and a revertible history.
[!TIP] Install (Claude Code) — clone, build, register:
git clone https://github.com/zuarbase/Zuar-Portal-MCP-Public.git ~/zuar-portal-mcp cd ~/zuar-portal-mcp && npm install && npm run build claude mcp add zuar-portal --scope user -- node ~/zuar-portal-mcp/dist/index.jsThen
cdto a project folder and run/portal-setupto connect it to a portal. Details ↓No terminal (Claude Desktop): download
zuar-portal-mcp.mcpbfrom the latest release and double-click it. Details ↓
At a glance
flowchart TB
CD["<b>MCP Client</b><br/>Claude Desktop · Claude Code · any MCP client"]
CD -- "JSON-RPC / stdio" --> S
subgraph server["zuar-portal-mcp server"]
direction TB
S["index.ts → buildServer()"]
S --> BT["🧱 <b>Block tools</b><br/>typed authoring + place_blocks"]
S --> RT["📦 <b>Resource tools</b><br/>generic CRUD · 18 kinds (blocks included)"]
S --> AT["⚡ <b>Action tools</b><br/>query · profile · users · config"]
S --> VC["🕓 <b>Version-control tools</b><br/>snapshot · history · diff · restore"]
S --> EL["🪄 <b>Setup & design</b><br/>configure_project · synthesize_theme"]
G{{"🛡️ <b>Safety & integrity gates</b><br/>write-domain · structure · refs · impact · SQL"}}
BT & RT & AT & VC & EL --> G
end
G --> HTTP["portalClient.ts<br/>login · X-Api-Key · retry · circuit breaker"]
HTTP -- "/api + /auth · HTTPS" --> P[("Zuar Portal")]
BT -. "mirrors every content write" .-> GIT[("git VC repo<br/>revertible")]
classDef gate fill:#fde68a,stroke:#b45309,color:#000;
class G gate
Every write is tagged with a risk domain and passes the safety gates before anything reaches the portal; every successful content write is mirrored to a git repo so it can be reverted.
Contents
- Highlights
- What Claude can do with it — the 38-tool catalog
- The Claude Code agent ecosystem — pipeline, agents, model/effort routing
- Guided onboarding & theming
- Requirements & credentials
- 🖱️ Install — Claude Desktop (one-click)
- ⌨️ Install — Claude Code & other clients
- Per-project configuration (multiple portals)
- Getting started
- Write safety & tool gating
- Resilience, observability & hardening
- Troubleshooting · Security · License
Working on the server rather than with it? See
CONTRIBUTING.md.
[!NOTE] 📚 Full documentation lives in
docs/— a 5-minute quickstart, install & config, a generated reference for all 40 tools, block authoring, the design system, version control, the in-blockzPortalAPI, the agent ecosystem & model routing, tool gating, safety gates, and troubleshooting.
Highlights
| 🧰 One uniform surface (v3.0.0) | One model for everything — blocks are a validated registry kind, one declarative place_blocks, uniform dry_run on every write, paged lists, and new reads (find_resource, get_references, vc_diff). Breaking; PORTAL_COMPAT_TOOLS=1 bridges the old v2 names. |
| ⌨️ Install once, use everywhere | Clone + build + claude mcp add … -- node …/dist/index.js, then /portal-setup per project. Or a one-click .mcpb for Claude Desktop — no terminal at all. |
| 🧱 Validated authoring | HTML blocks go through rule-checked tools (create_block/update_block/validate_block) — and create_resource (block) passes the same per-kind validator at the registry chokepoint, so footguns are caught before the portal is touched. |
| 🏢 Multi-portal, multi-repo (v2.4.0) | One install drives a different portal + git repo per folder via ./.zuar-portal/config.json. |
| 🪄 Browser setup, no JSON, no key in the model | configure_project serves a local loopback web form — you type the API key in your browser, so it never passes through the model; it validates live and writes a 0600 config. Falls back to elicitation / arguments. The design_intake prompt walks theming and drives synthesize_theme. |
| 🤝 A team of agents | In Claude Code, a gated build → style → responsive → debug → adversary → advisor pipeline of specialist subagents builds blocks for you — each on a right-sized model. |
| 🔒 Enterprise safety (v2.5–2.6) | Risk-domain write gating, least-privilege tool scoping, structural + referential integrity gates, pre-delete impact analysis, and an opt-in audit log. |
| 🕓 Revertible history (v2.2.0) | Every content write mirrors to a git repo — revert any change with restore_resource. |
What Claude can do with it
40 tools — one uniform surface ("a resource is a resource") across 11 capability groups. The full per-tool reference is generated from the live server (npm run gen:docs, so it can't drift): docs/03 · Tools Reference.
[!IMPORTANT] Upgrading from 2.x? v3.0.0 is a breaking redesign (48 → 37 tools). Every removed v2 name maps to a v3 primitive — see
CHANGELOG.md. SetPORTAL_COMPAT_TOOLS=1to temporarily register the old names as deprecated aliases that forward to the same gated v3 handlers.
🧱 Block tools — typed + validated
Blocks are now a first-class registry kind: list, fetch, and delete them with the generic resource tools (resource: "block"), and the block-authoring rules run as a per-kind validator at the registry write chokepoint — create_resource (block) is validated exactly like create_block. The typed fronts remain for ergonomic authoring:
| Tool | What it does |
|---|---|
validate_block |
Run the authoring rules against a block payload without writing — iterate until clean. |
create_block |
Create an HTML block (validated against authoring rules). |
update_block |
Update an HTML block — merged over the current block so untouched fields survive. |
File input
[4.2.0]—create_block,update_block, andvalidate_blocktakehtml_fileandcss_file. The server reads the bytes, so a model never carries them. Retyping a large block into a tool call is not a copy but a re-transcription: it burns tokens and silently normalizes characters (an em dash inside a regex character class arriving as a hyphen changes what the pattern matches — and passes review). Pass a path instead, edit the file in place, and only the delta is ever transcribed. Every gate runs identically either way. Reads are contained to the CWD, the VC dir, and anyPORTAL_FILE_ROOTSentry. |bind_block_query| Bind a block to a datasource/query (auto-creates the query); setsui_queries. | |place_blocks| One declarative placement primitive — add, update, hide, or remove blocks on a page grid in a single atomic write.mode: "merge"appends/updates (and honoursremove: [...]);mode: "replace"+confirm: truemakes the page exactly the given list while preserving survivors' customizedgrid.layoutsand hidden flags. |
<details> <summary><b>📦 Generic resource tools</b> — one CRUD surface over 18 resource kinds (blocks included)</summary>
Pass resource plus a body/id. Call describe_resource to see each resource's fields, required-to-create fields, supported verbs, and risk domain.
| Tool | What it does |
|---|---|
describe_resource |
List resources, or describe one (fields, verbs, domain). |
list_resource |
List records — always returns the paged envelope {total, offset, limit, returned, truncated, records} (default limit 100, max 500). |
get_resource |
Get one record by id — e.g. resource: "user", id: "me" for the current user's profile. |
find_resource |
Search by name (case-insensitive substring, or exact id) across kinds — optional kinds, tag, limit/offset; defaults to all non-admin kinds. |
get_references |
Read-only dependency query, both directions: dependents (who breaks if this is deleted — the same analysis the delete gate runs) and references (what this record points at, each flagged exists: false when dangling). |
create_resource |
Create a record (write-gated by domain; per-kind validators — blocks get the full authoring rules). |
update_resource |
Update a record (merged over current; write-gated). |
delete_resource |
Delete a record (write-gated; pre-delete impact analysis; confirm/force). |
validate_portal |
Read-only sweep for malformed records, dangling refs, and risky SQL. |
Covered resources: block, layout (pages), datasource, query, db_modification, partial, theme, snippet, translation, dashboard, tag, user, group, permission, access_policy, api_key, credential, system.
Every write tool accepts
dry_run: true— every gate runs (domain, structure, per-kind rules, references, impact), nothing is written, and the response carriesapplied: falseplus what would have been written.
</details>
<details> <summary><b>⚡ Action tools</b> — data exploration, users, config, naming, setup, design & migration</summary>
| Tool | What it does | Domain |
|---|---|---|
profile_datasource |
Per-column stats (type, distinct values, min/max) plus raw sample rows (sample.columns / sample.rows; sample_rows default 10, max 50) to design filters + charts against real columns. |
read |
execute_query |
Run a saved query by id and return results (optional row limit). |
read |
run_db_modification |
Run a saved DB write by name. Needs confirm: true. |
data |
change_password |
Change the current user's password. | admin |
get_user_access |
Read a user's group membership and permissions in one call. | read |
set_user_access |
Replace a user's groups and/or permissions — each provided list is a full replace; needs confirm: true, supports dry_run. |
admin |
get_config / update_config |
Read / set portal config by path. | read / admin |
get_version |
Portal version + about (capability check). | read |
get_rules |
Show active block-authoring rules. | read |
naming |
The scope · kind · subject naming grammar — action: "suggest" proposes names, action: "parse" decomposes them. |
read |
check_connection |
Start here — confirm the connection in one authenticated round-trip: portal, version, who you're signed in as, binding state and write posture, in ~160 chars. Bad credentials return the reason and the fix, not a false success (always available). | read |
get_capabilities |
Report the current posture — enabled/disabled tool groups, write-safety, VC + audit status, and the active config (portal / VC repo, secrets redacted) under its config key (always available). |
read |
get_metrics |
Per-tool call count, error rate, latency, uptime, breaker state (always available). | read |
configure_project |
Connect this folder to a portal. By default it opens a local loopback setup page (API key typed in the browser, never through the model), validates live, writes a 0600 config; falls back to elicitation then arguments (pass ui:false, or portal_url + api_key + user_id). Also collects write-safety toggles + access scope + optional GitHub VC. Pins the folder to the portal so writes can't cross portals; writes ./.zuar-portal/config.json + design.md + a managed CLAUDE.md block. |
setup |
reload_config |
Re-read config from disk (project/bundle/env) without a restart; resets the portal session. | setup |
synthesize_theme |
Pure theme synthesis — preferences (± an SSRF-guarded website color fetch) → a token map plus the exact create_resource call (create_with); creates nothing itself. Orchestrated by the design_intake prompt. |
design |
migration_preflight |
Read-only migration audit — every block classified by function, all code-bearing fields scanned (comment/string-aware), placement via layouts/partials/snippets, hardcoded-origin sweep, bound-query preflight, browser-probe checklist. | migration |
repair_query_metadata |
Refresh a saved query's stored column metadata from a live execution — SQL untouched, dry-run by default, round-trip verified. | migration |
The current user's profile is plain resource CRUD now:
get_resource/update_resourcewithresource: "user", id: "me". Guided migration scoping is themigration_kickoffprompt (see Prompts below).
</details>
<details> <summary><b>🕓 Version-control tools</b> — snapshot, history, restore <i>(v2.2.0)</i></summary>
| Tool | What it does |
|---|---|
vc_status |
Show whether VC is configured and the repo state. |
snapshot_portal |
Commit the full current portal state to the git repo — a durable checkpoint. |
vc_log |
Show the commit history of content changes. |
vc_diff |
Unified diff between two committed versions — record-scoped (resource + id) or repo-wide; defaults to the previous commit touching the record vs HEAD. Inspect before restore_resource — a revert is never blind. |
restore_resource |
Restore a resource to a previous committed version. |
See docs/07 · Version Control.
</details>
Resources (zportal://guide/*) — authoring guidance Claude reads before building, so blocks follow zPortal conventions even on a fresh machine: block-structure, currentblock, zportal-api, charting, conventions, design-system, visual-verification, migration-1.18, loading-overlay, migration-playbook, and block-performance.
Prompts — guided workflows now live here rather than in the tool surface (9): zuar_portal_start (the cheapest session opener — confirm the connection in one call, report one line, ask what's next), zuar_portal_quickstart (orient → route), create_zportal_block (discover → build → create), setup_zuar_project (connect this folder, routes to configure_project), migrate_block_to_118 (one legacy block → the 1.18 lifecycle), add_loading_overlay (the sanctioned spinner + fade-out), block_perf_pass (the large-dataset perf audit: measure → trim SELECT * → chart-lib prefetch → 60 s honest timeouts), design_intake (guided theming — walks brand/website/density/radius, drives synthesize_theme, then creates the theme via create_resource), and migration_kickoff (guided migration scoping — batches the scope decisions, writes .zuar-portal/migration-scope.json, then runs migration_preflight).
The Claude Code agent ecosystem
When this repo is your Claude Code working directory, the MCP tools come with a team of specialists in .claude/. You don't drive create_block/bind_block_query by hand — you describe what you want, and a gated pipeline builds, styles, hardens, and reviews it. Full guide: docs/13 · Agents & Workflows.
The block pipeline
Blocks are never shipped raw. A spec flows through quality gates, each a focused subagent:
flowchart LR
spec([spec]) --> B["🏗️ builder"] --> St["🎨 stylist"] --> R["📱 responsive"] --> D["🔧 debugger"]
D --> A{"🚨 adversary<br/><b>CODE GATE</b>"}
A -- "blocking (≤2 rounds)" --> D
A -- "clean" --> V{"👁️ visual<br/><b>GATE</b>"}
V -- "blocking (≤2 rounds)" --> D
V -- "clean / skipped" --> Ad["🧭 advisor"] --> ship([ship ✅])
classDef gate fill:#fde68a,stroke:#b45309,color:#000;
classDef ro fill:#dbeafe,stroke:#1d4ed8,color:#000;
class A,V gate
class Ad ro
The adversary (gate) red-teams the block and proves each finding with evidence; while it returns blocking findings the pipeline loops back to the debugger. The visual gate (the adversary with browser eyes) then opens the rendered block in Claude for Chrome — screenshot, console, network — and a blank render, console error, sample-not-live data, or overflow loops back to the debugger too; it's best-effort and skips with a note when the extension isn't connected or the block isn't on a page. The advisor asks "is this the right block?" All gates are read-only — they carry no write tools and physically cannot mutate the portal (browsing/screenshotting is read-only). Beyond the six pipeline agents, four specialists handle broader jobs: portal-data-expert, portal-theme-designer, portal-bulk-operator (snapshot-first), and portal-onboarding.
Seeing the portal (Claude for Chrome)
Every gate above reasons about a block from its code and query rows — but a block can validate, bind, and still render blank, throw a runtime console error, overflow its grid cell, or silently show its hardcoded sample fallback instead of live data. With the Claude for Chrome extension connected, the agents can see the portal: open the page, screenshot the block, and read the browser console/network — for visual debugging and a final visual sign-off.
- Recorded at setup.
configure_projectasks whether you use Claude for Chrome and storesbrowser.claudeInChromein./.zuar-portal/config.json;get_capabilitiesreports it. - Used where it pays. The debugger looks before it guesses; the adversary owns the visual gate; the stylist and responsive-specialist screenshot their work (the latter steps widths with
resize_window); the advisor checks it reads at a glance. - Sign-in caveat. The MCP authenticates with an API key, but the browser needs a logged-in session — to view private pages you must be signed into your portal in Chrome. The MCP can't log you in.
- Graceful by design. No extension, or the block isn't on a page? Every agent falls back to code-only review and says so. The doctrine lives in the
zportal://guide/visual-verificationresource.
Slash commands
| Command | What it runs |
|---|---|
/portal-setup |
First-time per-folder setup + alignment Q&A → config + project brief. |
/portal-build <spec> |
The full build→style→responsive→debug→adversary→advisor pipeline for one block. |
/portal-theme <goal> |
Design or apply a portal-wide theme. |
/portal-bulk <change> |
A guarded bulk change across many blocks/pages (snapshot → dry-run → atomic apply). |
/portal-audit [filter] |
Read-only audit of existing blocks — bugs, a11y, responsiveness, design fit. |
/portal-improve |
One bounded improvement pass: score → fix worst blocks → verify → sweep → provable delta. Safe to schedule nightly. |
/portal-align |
Run the alignment Q&A on its own. |
Model & effort routing
Each agent runs on the model and reasoning effort that fit its job — sharp where judgment matters, cheap where the work is mechanical. Three composing layers:
1 · Agent defaults (model:/effort: frontmatter) — for a direct call (a fast surgical edit, or one agent dispatched from a command):
| Tier | Agents | Model · effort |
|---|---|---|
| 🧠 Judgment / data | data-expert, adversary, advisor | opus · high |
| 🛠️ Authoring | builder, stylist, debugger, bulk-operator, theme-designer, onboarding | sonnet · medium |
| ⚡ Mechanical | responsive-specialist | haiku · low |
2 · Workflow tier toggle — portal-block-pipeline.js and portal-audit.js take args:{ …, tier } and set each stage's model/effort explicitly:
tier |
For… | Builders | Judgment gates |
|---|---|---|---|
fast |
cheap iteration, throwaway drafts, triage | sonnet/haiku · low | sonnet · medium |
standard (default) |
a normal build / audit | sonnet · medium | opus · high |
max |
production / executive build, pre-release audit | opus · high | opus · xhigh |
3 · Commands pin to sonnet · medium — they only orchestrate (pre-flight → dispatch → synthesize); quality lives in the agents/workflow they call. /portal-build and /portal-audit infer the tier from your phrasing.
The MCP server never selects a model — only the agents, commands, and workflows that drive it do. Re-tier via agent frontmatter or a workflow's
ROUTINGtable; see.claude/README.md.
Guided onboarding & theming
By default configure_project serves a tiny loopback web form (http://127.0.0.1:<random-port>)
from the MCP process, best-effort opens your browser, and returns the link immediately — you type the
URL, API key, write-safety toggles, access scope, and version control in the browser, so the API
key never passes through the model. On Save it validates live, writes the gitignored 0600 config,
and applies the change live. It falls back to MCP elicitation (field-by-field prompts) and then
to arguments when a browser can't be used — pass ui:false, or pass portal_url + api_key +
user_id as arguments. Guided theming is the design_intake MCP prompt, which orchestrates the pure
synthesize_theme tool. See The browser setup form.
flowchart TB
subgraph setup["🔌 configure_project — connect a portal"]
direction TB
s1["Portal URL"] --> s2["API key 🔒"] --> s3["User ID"] --> s4{"add GitHub VC?<br/>(optional)"}
s4 --> s4b{"use Claude for Chrome?<br/>👁️ visual checks"}
s4b --> s5["✓ live portal login<br/>✓ GitHub token + repo (API)"] --> s6[["writes .zuar-portal/config.json<br/>+ .gitignore"]]
end
subgraph intake["🎨 design_intake prompt — theme the portal"]
direction TB
d1["brand + website"] --> d2["fetch site 🛡️ SSRF-guarded<br/>→ suggest brand colors"] --> d3["palette · density · radius"]
d3 --> d4["header + sidebar style"] --> d5{"confirm?"} --> d6[["synthesize_theme →<br/>create_resource (theme)"]]
end
configure_projectrefuses to clobber an existing config, validates with a real login, and writes a gitignored./.zuar-portal/. It also asks whether you use Claude for Chrome (stored asbrowser.claudeInChrome) so the build pipeline can see your blocks render — visual debugging + a final visual gate (see Seeing the portal). Thesetup_zuar_projectprompt and/portal-setuproute to it; passinteractive: falsefor the direct, no-prompt path. (Replaces v2'ssetup_portalandinit_project_config.)- The
design_intakeprompt fetches the brand's website throughsynthesize_theme's SSRF-guarded fetch to suggest a palette, then walks density/radius/header/sidebar and, on your confirmation, creates athemeresource viacreate_resource.synthesize_themeitself is pure — it returns the token map and the exactcreate_withcall, and never writes.
Requirements
- A Zuar Portal reachable over HTTPS, with an account that can manage blocks (admin recommended).
- Node.js 18+ — for Claude Code and any other MCP client. (Claude Desktop's one-click
.mcpbbundles its own Node, so you need nothing.)
Getting your portal credentials
You need three values, entered once during install.
| # | Value | Where |
|---|---|---|
| 1 | Portal URL | The base URL, no trailing path — e.g. https://your-portal.zuarbase.net. |
| 2 | Portal API Key | Admin → Auth → API Keys → create/copy a key. It inherits its user's permissions — that user must be able to create/edit/delete blocks. |
| 3 | Portal User ID | Admin → Users → your user → copy the UUID from the page URL. |
[!IMPORTANT] Keep the API Key and User ID private. In the Claude Desktop bundle they're declared sensitive (masked, stored securely) and never leave the machine running the server.
Install — Claude Desktop (one-click)
- Download
zuar-portal-mcp.mcpbfrom the latest release. - Double-click it, or drag it onto the Claude Desktop window. An install dialog appears.
- Fill in Portal URL, Portal API Key, Portal User ID (and optionally the write-safety toggles).
- Confirm. The tools, resources, and prompts are now available to Claude.
To update later, install a newer .mcpb over the old one.
Install — Claude Code & other MCP clients
This server speaks MCP over stdio, so any MCP-capable client can use it. Clone it, build it once, and register the built entry point — you need Node ≥ 18 and git.
Register it once, for every project:
git clone https://github.com/zuarbase/Zuar-Portal-MCP-Public.git ~/zuar-portal-mcp
cd ~/zuar-portal-mcp
npm install
npm run build
claude mcp add zuar-portal --scope user -- node ~/zuar-portal-mcp/dist/index.js
[!IMPORTANT] Keep the clone where it is.
claude mcp addrecords the absolute path todist/index.js, so moving or deleting the folder breaks the server with aspawn ENOENT. Pick a permanent home for it — not/tmp, not~/Downloads.
To update: pull and rebuild in place. The path doesn't change, so there's nothing to re-register — but restart your client, since the tool list is only fetched at handshake.
cd ~/zuar-portal-mcp && git pull && npm install && npm run build
Then, in each portal project folder, connect it to that portal:
mkdir ~/work/acme-portal && cd ~/work/acme-portal
claude
> /portal-setup
/portal-setup asks for your three values, checks them with a real login, and writes a gitignored ./.zuar-portal/config.json. Every folder can point at a different portal — see Per-project configuration ↓.
<details> <summary>Prefer to write the config file by hand?</summary>
.mcp.json in the project (or claude_desktop_config.json) — point args at your clone's built entry point, as an absolute path (~ is not expanded here):
{
"mcpServers": {
"zuar-portal": {
"command": "node",
"args": ["/Users/you/zuar-portal-mcp/dist/index.js"]
}
}
}
</details>
[!IMPORTANT] Leave
envempty. Don't putPORTAL_URL/PORTAL_API_KEY/PORTAL_USER_IDin your client config. Credentials in the client'senvmap create one global portal that every project silently inherits — so a folder you believe is pointed at staging quietly publishes to production. Let/portal-setupwrite credentials per project instead. Each project then carries a binding fingerprint, and the server refuses a write aimed at a portal the folder isn't bound to. Env vars still work (useful for CI, or a single-portal install), but the project file is the path that can't surprise you.
Per-project configuration (multiple portals)
One MCP install can drive a different portal — and a different git state-repo — in every folder. At startup the server resolves credentials in layers, highest priority first:
flowchart LR
A["1 · Project config<br/><code>./.zuar-portal/config.json</code><br/>(walks up from cwd)"] --> R{{"resolved<br/>credentials"}}
B["2 · Environment<br/><code>PORTAL_*</code> env vars<br/>(Desktop / MCPB)"] --> R
C["3 · Bundle config<br/><code>config.json</code> beside bundle"] --> R
Most settings resolve per field, so a project file can set just vc.dir and inherit the rest. Empty values are ignored, so a blank Desktop field never shadows a project value.
[!WARNING] Portal credentials are the exception — they resolve all-or-nothing from ONE layer (v4.0.0). A layer naming any of
url/apiKey/userIdmust supply all three, or startup fails with an explicit error. Per-field layering here was a cross-portal hazard: a project with aurlbut noapiKeysilently borrowedPORTAL_API_KEYfrom the environment — one portal's address paired with another's key.
The file uses one schema for both the portal and its VC repo:
{
"portal": { "url": "https://team-a.zuarbase.net", "apiKey": "…", "userId": "…" },
"vc": { "dir": "/path/to/team-a-state", "push": true,
"remote_url": "https://github.com/you/team-a-portal-state.git", "token": "…" }
}
Set it up without hand-editing JSON: ask Claude to run configure_project (see Guided onboarding ↑). get_capabilities shows which portal/repo is in effect under its config key (secrets redacted). ./.zuar-portal/ is gitignored, so credentials are never committed.
Getting started
New portal? Start here. From the folder you want to work in:
cd ~/work/acme-portal
claude
> /portal-setup
One command. It connects the folder to the portal (writing gitignored credentials + a binding fingerprint), profiles your datasources, interviews you about the business, and writes a project brief the other agents read. Everything below assumes it's done.
Then just talk to Claude:
- Confirm the connection — "List the datasources on my portal." →
list_resource (datasource). - Look at real data — "Show me a few sample rows from the Sales datasource." →
profile_datasource(per-column stats + raw sample rows, so Claude sees the real column names first). - Create a block — "Create a stat-card block 'Total Orders' showing the order count from Sales." → reads
zportal://guide/*, builds the two-field block,create_block, reports the UUID. - Iterate — "Make the number bigger and use the portal's primary color." / "Turn it into a bar chart of orders by state." →
update_block.
[!TIP] In Claude Code, run
/portal-build "a stat card of total orders from Sales"to push the spec through the whole gated pipeline, or invoke thecreate_zportal_blockprompt for a structured discover → build → create flow.
Write safety & tool gating
Every write is tagged with a risk domain, gated independently:
| Domain | Covers | Default | Enable with |
|---|---|---|---|
content |
blocks, layouts, partials, themes, queries, snippets, translations, dashboards, tags | on | (on unless read-only) |
data |
datasources, db_modifications, run_db_modification |
off | PORTAL_ALLOW_DATA_WRITES=1 |
admin |
users, groups, permissions, access policies, API keys, credentials, system, config, passwords | off | PORTAL_ALLOW_ADMIN_WRITES=1 |
PORTAL_READONLY=1disables every write — reads and discovery still work.- A blocked write returns a clear message naming the flag to set; nothing reaches the portal.
run_db_modificationadditionally requiresconfirm: trueon every call.- Deletes and user/password mutations are marked destructive to MCP clients.
- Uniform
dry_run: true(v3.0.0) on every write tool — all gates run (domain, structure, per-kind rules, references, impact) but nothing is written; the response reportsapplied: falseand what would have changed. A dry run never bypasses a gate.
Least-privilege tool scoping (v2.5.0) — disable whole capability groups with PORTAL_DISABLE_TOOLS=users,config, or stand up a build-only allowlist with PORTAL_ENABLE_TOOLS=blocks,resources,data (deny wins).
Upgrading from 2.x — PORTAL_COMPAT_TOOLS=1 (off by default) registers the removed v2 tool names as deprecated aliases in a compat group; each alias forwards to the same gated v3 handler, so aliases confer no extra capability.
Integrity gates (v2.5–2.6, server-side, cannot be bypassed) — every content write is checked for portal-compatible structure (a page missing grid.layouts is repaired or rejected) and dangling references; deletes run pre-delete impact analysis and refuse to orphan dependents unless force=true; user deletes refuse to remove the last admin; unscoped mass SQL (a destructive verb without a real WHERE — 1=1 doesn't count — plus MERGE/TRUNCATE/DROP/ALTER…DROP/GRANT) needs allow_unfiltered=true, and the check fails closed when the SQL can't be inspected. Run the read-only validate_portal anytime to sweep for problems. Full guide: docs/16 · Safety & Integrity.
Unattended-loop safety (unreleased) — the guarantees that make parallel/overnight agent loops safe to leave alone:
- No duplicate creates: a create that fails ambiguously (network died after the request may have landed) is verified by name and adopted if it already exists — never blindly resent.
run_db_modificationis never auto-retried at all. - No lost updates: pass
expected_updated_at(from your read) toupdate_resource/update_block/place_blocksand a mid-flight change by anyone else refuses with a conflict instead of silently overwriting. - No stranded cruft: name loop temporaries
TMP · <purpose>(or tag themscratch), then sweep withcleanup_scratch— dry-run by default, deletes only unreferenced non-recent candidates through the gated path, content stays VC-restorable. - Provable improvement:
score_portalscores every block/page 0–100 on the mechanically checkable and diffs against a saved baseline — a loop's regression is a per-record delta, not an anecdote. - Admin can't self-destruct: stripping your own admin access needs
allow_self_lockout=true;update_configandchange_passwordrequireconfirm(config edits returnprevious_at_pathfor one-call revert).
In the Claude Desktop bundle these are install-dialog toggles; for other clients set them as env vars. Deeper dive: docs/14 · Tool Gating & Guidance.
Resilience, observability & hardening
Production-grade behaviour for a local, single-user server — safe defaults, no configuration required.
Resilience (the portal HTTP client every tool calls through):
| Behaviour | Default | Tune with |
|---|---|---|
| Per-attempt timeout | 30 s | PORTAL_TIMEOUT_MS |
Retries on transient failure (network, 408/425/429/5xx), exp. backoff + jitter, honouring Retry-After |
2 | PORTAL_MAX_RETRIES, PORTAL_BACKOFF_BASE_MS, PORTAL_BACKOFF_MAX_MS |
| Circuit breaker — fail fast while the upstream is down | opens after 5 failures, 15 s cooldown | PORTAL_BREAKER_THRESHOLD, PORTAL_BREAKER_COOLDOWN_MS |
| Max request body size | 5 MB | PORTAL_MAX_BODY_BYTES |
| Max tool input size (rejected at the MCP boundary) | 2 MB | PORTAL_MAX_INPUT_BYTES |
execute_query returned-row cap |
1,000 rows (limit:0 = all) |
per-call limit |
list_resource page byte cap (auto-projects to {id,name} + note) |
60k chars | PORTAL_LIST_BYTE_CAP (0 disables) |
| Result serialization | compact JSON | PORTAL_PRETTY_JSON=1 for pretty |
| Binding re-verification cadence | 10 min + on config reload | PORTAL_BINDING_REVERIFY_MS |
| Read cache (GETs; any write clears it; write-critical reads bypass it) | 5 s TTL | PORTAL_READ_CACHE_MS (0 disables) |
Retry safety: GET retries on any transient signal; writes retry only on explicit 429/503 back-pressure or a network failure that provably never connected — never on an ambiguous 502/504, and a create whose network error may have delivered the request is verified by name and adopted rather than resent (no silent duplicates).
Observability — every call gets a request id, latency, and an error tally. get_metrics (always-on) reports per-tool counts, error rate, latency, uptime, and the breaker state — metadata only, no payloads or secrets. Set PORTAL_LOG_FORMAT=json for structured stderr logs; PORTAL_AUDIT_LOG appends metadata-only JSONL for every content/data/admin write.
Output secret redaction — secret-bearing fields (password, secret, token, api_key, …) are masked as [redacted] on resource reads, so they never flow into the model's context, and secrets are also caught by shape inside any value or name — connection-string passwords (postgresql://user:[redacted]@host), JWTs, PEM private keys, AWS key ids — including in execute_query / profile_datasource results. Generic hex and key=value shapes are deliberately not masked (uuids, git shas and SQL params are legitimate content that round-trips back into writes). Identifier *_id fields are never masked; create/update responses are intact (so a freshly generated secret can be seen once). Disable with PORTAL_REDACT_SECRETS=0. Portal-authored content additionally returns inside an untrusted-data envelope, so a poisoned record name reads as data, not instructions.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
"failed to connect" / spawn ENOENT |
The client couldn't spawn the command. Either (a) the clone moved or was deleted — claude mcp add stored an absolute path to dist/index.js; re-check it with claude mcp list; (b) you registered it before running npm run build, so dist/index.js doesn't exist — build, then restart the client; or (c) node isn't on the PATH your client sees (common with nvm + Claude Desktop, which doesn't load your shell profile — use the .mcpb there, it bundles its own runtime). |
An old guide says to run npx -y zuar-portal-mcp-server |
That package is not on npm — the command can't work and fails as ENOENT. Install from a clone: Install — Claude Code ↓. |
Reads work, but every write is refused as unbound |
This folder isn't bound to a portal. Run /portal-setup (or configure_project) in it. Binding is what stops one project's block being published to another's portal, so it's deliberate — see Per-project configuration. |
| "Missing portal credentials: …" | One of PORTAL_URL / PORTAL_API_KEY / PORTAL_USER_ID is blank. Re-enter it. |
| "…must supply all three of url/apiKey/userId" | A config layer names some credentials but not all. That's refused on purpose — a url without an apiKey used to silently borrow the key from your environment, pairing one portal's address with another's key. Supply all three in one place. |
| "Portal login failed: HTTP 401/403" | Wrong API key or user ID, or the user lacks permission. Regenerate the key; confirm the user can manage blocks. |
list_resource (query) says the endpoint isn't available |
Your portal predates the saved-queries API (1.18+). Use resource: "datasource" — expected, not an error. |
| Tools don't appear in Claude | Restart the client — the tool list is fetched once at handshake, so a newly added server (or a new version) won't show up in a running session. For the .mcpb, reinstall it. |
| You upgraded but the new tools/rules aren't there | Same cause: the client is still running the old process. Restart it. |
A v2 tool name is missing (list_blocks, setup_portal, …) |
v3.0.0 removed/renamed it — see CHANGELOG.md for the replacement, or set PORTAL_COMPAT_TOOLS=1 for deprecated forwarding aliases. |
| Want to see what it's doing | Set PORTAL_DEBUG=1 (or PORTAL_LOG_FORMAT=json). Logs go to stderr only. |
| "circuit breaker is open" | The upstream failed repeatedly; it auto-recovers after a short cooldown. get_metrics shows breaker state. |
A stored secret returns [redacted] |
Read redaction is on. Set PORTAL_REDACT_SECRETS=0 for that session. |
More: docs/12 · Troubleshooting.
Security
- Credentials are never logged. Debug output (gated by
PORTAL_DEBUG=1) goes to stderr only, so it never corrupts the MCP stdio stream. - The API Key and User ID are declared sensitive in the bundle manifest.
- The server talks only to the Portal URL you configure; the base URL is validated as a well-formed
http(s)origin at startup.synthesize_theme's website fetch (driven by thedesign_intakeprompt) is SSRF-guarded. create_block/update_blockare restricted totype: "html"and reject other types before any portal call.- Secret-bearing fields are redacted from reads; tool inputs and request bodies are size-capped.
See SECURITY.md for the full posture: the per-tool-group data-touch matrix, credential handling, network egress, and data retention.
License
MIT.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。