Skip to main content

OpenClaw

Status (chat UI path): ✅ Verified on OpenClaw 2026.4.18 with helmdeck 1b91f6c. Gateway chat UI at http://localhost:18789 sees the full 36-pack catalog (prefixed helmdeck__) and SSE handshake succeeds. Status (CLI path): ⚠️ Regressed on OpenClaw ≥ 2026.4.18. openclaw agent --agent main … does not load bundled MCP tools — only 24 built-in tools appear, no helmdeck__*. Suspect upstream commit: 0e7a992d (fix(agents): filter bundled tools through final policy). Use the chat UI for end-to-end agent runs until upstream fixes the CLI path. Last full end-to-end green: 2026-04-10 with OpenClaw 2026.4.10 + helmdeck v0.6.0.

Validation history: Originally validated via scripts/validate-openclaw.sh — 9 packs tested through OpenClaw → SSE MCP → helmdeck round trip with openrouter/auto as the LLM. Packs validated: http.fetch, browser.screenshot_url, web.scrape_spa, slides.render, browser.interact, github.list_prs, github.list_issues, github.search, repo.fetch + fs.list chain. Additionally validated via direct REST: full code-edit loop (repo.fetchfs.writefs.patchfs.readcmd.rungit.commitrepo.push) + all GitHub write packs (create_issue, post_comment, create_release) + python.run + node.run.

Upgrading OpenClaw? Follow openclaw-upgrade-runbook.md — covers the git pull sequence, JWT / SKILLS.md preservation checks, and the regression triage steps for the CLI-path issue above.

Topology

OpenClaw is Topology A — both OpenClaw and helmdeck run as docker compose stacks on the same host, joined onto a shared bridge network so OpenClaw resolves helmdeck-control-plane by service-name DNS.

┌──── helmdeck_default network ─────────┐
│ helmdeck-control-plane:3000 │
│ ┌────────────────────────────┐ │
│ │ /api/v1/mcp/sse (MCP) │ │
│ │ /v1/chat/completions (LLM) │ │
│ └────────────────────────────┘ │
│ ▲ │
│ │ HTTP, JWT-protected │
│ openclaw-gateway:18789 │
└───────────────────────────────────────┘

Prerequisites

  • Docker + docker compose v2
  • Helmdeck cloned at /root/helmdeck (or wherever)
  • ≥ 4 GB RAM, ≥ 2 CPUs (the install script preflight enforces this)

1. Install helmdeck

git clone https://github.com/tosin2013/helmdeck.git
cd helmdeck
./scripts/install.sh

The script generates a .env.local with strong random secrets, builds every binary + the React UI, brings the compose stack up, polls /healthz, and prints the admin password. Save it.

2. Install OpenClaw

git clone https://github.com/openclaw/openclaw.git
cd openclaw
OPENCLAW_GATEWAY_BIND=lan ./scripts/docker/setup.sh

This builds the OpenClaw image, runs the onboarding flow, and brings up openclaw-gateway on port 18789. The setup script prints the gateway token at the end — save it.

OpenClaw's Control UI requires HTTPS or localhost (WebCrypto secure-context check). For remote access, the simplest path is an SSH tunnel from your workstation:

ssh -L 18789:localhost:18789 -L 3000:localhost:3000 root@<server>

Then open http://localhost:18789 and http://localhost:3000 in your browser — both are now treated as secure-context localhost.

3. Join the networks

Helmdeck ships an overlay file that merges OpenClaw's compose stack onto helmdeck's bridge network:

docker compose \
-f /root/openclaw/docker-compose.yml \
-f /root/helmdeck/deploy/compose/compose.openclaw-sidecar.yml \
up -d openclaw-gateway

After this, openclaw-gateway can resolve helmdeck-control-plane:3000 via DNS.

4. Configure helmdeck as an MCP server in OpenClaw

Two paths — pick whichever you prefer:

docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
mcp set helmdeck '{"url":"http://helmdeck-control-plane:3000/api/v1/mcp/sse","headers":{"authorization":"Bearer <your-helmdeck-jwt>"}}'

The CLI writes to ~/.openclaw/openclaw.json and validates the shape against OpenClaw's config schema before saving — preferred over hand-editing because the schema occasionally shifts between OpenClaw releases.

4b. Edit ~/.openclaw/openclaw.json directly (advanced)

OpenClaw stores MCP servers at the top level of the config under mcp.servers, keyed by server name (NOT under each agent):

{
"gateway": { "...": "..." },
"agents": { "...": "..." },
"mcp": {
"servers": {
"helmdeck": {
"url": "http://helmdeck-control-plane:3000/api/v1/mcp/sse",
"headers": {
"authorization": "Bearer <your-helmdeck-jwt>"
}
}
}
}
}

Then restart openclaw-gateway to pick up the change:

docker compose -f /root/openclaw/docker-compose.yml restart openclaw-gateway

Mint the JWT

JWT=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"<from install.sh>"}' | jq -r .token)
echo "$JWT"

Paste the value into the Authorization header above.

5. Configure OpenClaw's LLM provider

OpenClaw needs its own LLM credentials. The easiest path is OpenRouter (which is also what helmdeck routes to in the validation walkthrough):

docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
models auth login openrouter

Follow the prompts to paste your OpenRouter API key. Then set the active model:

docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
models use openrouter/minimax/minimax-m2.7

Helmdeck-as-LLM-gateway path: OpenClaw's docs do not clearly document a custom OpenAI-compatible base URL escape hatch as of v0.6.0 of helmdeck. If we confirm via inspection of models.json that an arbitrary base_url works, this section will gain a "Route OpenClaw's LLM through helmdeck" subsection that points OpenClaw at http://helmdeck-control-plane:3000/v1/chat/completions so the T607 success-rate panel lights up from OpenClaw runs. Until then, OpenClaw uses its OpenRouter key directly and helmdeck only sees the MCP tool calls.

6. Walk the Phase 5.5 code-edit loop

Open http://localhost:18789 in your browser, paste the OpenClaw gateway token into Settings, then send a chat prompt:

Use the helmdeck packs to:

  1. repo.fetch git@github.com:<me>/<fixture-repo>.git using vault credential gh-deploy-key.
  2. fs.list the clone for *.md files.
  3. fs.read the README and propose a one-line edit.
  4. fs.patch to apply the edit (literal search-and-replace).
  5. cmd.run go test ./... (or any project check) in the clone.
  6. git.commit with message chore: helmdeck integration smoke.
  7. repo.push back to origin.

Pass criteria:

  • The new commit lands on the remote branch.
  • The Audit Logs panel in the helmdeck UI (http://localhost:3000) shows one entry per pack call, in order.
  • The SSH private key never appears in OpenClaw's chat transcript — only the ${vault:gh-deploy-key} placeholder.

If all three hold, update the status banner at the top of this file to ✅ with today's date + the helmdeck version, and flip the matching row in README.md.

Known issue: header key MUST be lowercase authorization

Status: Confirmed against OpenClaw 2026.4.10 + @modelcontextprotocol/sdk@1.29.0 + eventsource@3.0.7. Filed upstream as a draft issue at docs/integrations/openclaw-upstream-issue.md.

If you write the helmdeck MCP server config with capital-A Authorization:

{ "url": "...", "headers": { "Authorization": "Bearer <jwt>" } }

…OpenClaw's bundle-mcp will fail to connect to helmdeck with:

[bundle-mcp] failed to start server "helmdeck" (.../api/v1/mcp/sse): Error: SSE error: Non-200 status code (401)

Helmdeck's audit log shows the request as GET /api/v1/mcp/sse → 401.

Why

OpenClaw's buildSseEventSourceFetch (/app/dist/content-blocks-k-DyCOGS.js) merges the user's headers over the SDK's headers as a plain JS object via spread:

return fetchWithUndici(url, {
...init,
headers: { ...sdkHeaders, ...headers } // sdkHeaders from Headers iteration → lowercase keys
});

The MCP SDK returns headers as a Headers instance, and iterating it yields lowercase keys per the spec — so sdkHeaders ends up with authorization. When the user config has Authorization (capital), the spread produces a plain object with two distinct keys:

{ accept: "text/event-stream", authorization: "Bearer <jwt>", Authorization: "Bearer <jwt>" }

Undici then constructs a Headers list from that object using append, which comma-joins duplicates (per the Fetch spec) into:

Authorization: Bearer <jwt>, Bearer <jwt>

Helmdeck's bearer-token parser (and any standards-compliant parser) rejects this malformed header with 401.

Workaround (until upstream fix)

Use lowercase authorization as the key in your OpenClaw helmdeck config:

docker compose -f /root/openclaw/docker-compose.yml run --rm openclaw-cli \
mcp set helmdeck '{"url":"http://helmdeck-control-plane:3000/api/v1/mcp/sse","headers":{"authorization":"Bearer <jwt>"}}'

This makes OpenClaw's spread merge into a single authorization entry, which undici then sends as a single well-formed Authorization header.

Upstream fix (proposed)

OpenClaw's buildSseEventSourceFetch should construct a Headers instance and use .set() (which is case-insensitive and replaces) instead of plain-object spread:

function buildSseEventSourceFetch(headers) {
return (url, init) => {
const merged = new Headers(init?.headers ?? {});
for (const [k, v] of Object.entries(headers)) merged.set(k, v);
return fetchWithUndici(url, { ...init, headers: merged });
};
}

This eliminates the case-collision regardless of how the user wrote the key.

Webhook callback — push pack results to OpenClaw

Heavy packs (slides.narrate, research.deep, content.ground) automatically return a SEP-1686 task envelope so the JSON-RPC request never blocks. Most clients SHOULD just poll, but if you want true push semantics — the LLM's next turn fires when a pack completes, no polling at all — wire up the webhook receiver bundled in examples/webhook-openclaw/.

Architecture

┌─────────────────┐ 1. tools/call slides.narrate
│ OpenClaw LLM ├─────────────────────────────────┐
└─────────────────┘ ▼
┌──────────────────┐
│ helmdeck-control │
│ -plane (MCP) │
└────────┬─────────┘
3. POST /helmdeck-callback │
X-Helmdeck-Signature: sha256= │ (60-180s
┌─────────────────────────────────┘ later)

┌─────────────────┐ 4. POST /chat/inject
│ helmdeck-callback ├──────────┐
│ (Node, ~50 LOC) │ ▼
└─────────────────┘ ┌──────────────────┐
│ OpenClaw chat- │
│ injection API │
└──────────────────┘

5. LLM sees: "[helmdeck] Pack
completed. Video: <url>"

Step 2 (helmdeck task envelope returns immediately) and step 3 (helmdeck POSTs the result when done) are independent — the LLM doesn't have to wait or poll.

Setup

  1. Run the receiver alongside helmdeck and OpenClaw:

    # in your docker-compose override
    services:
    helmdeck-callback:
    build: ./examples/webhook-openclaw
    environment:
    OPENCLAW_INJECT_URL: http://openclaw-openclaw-gateway-1:3210/api/chat/inject
    WEBHOOK_SECRET: ${HELMDECK_WEBHOOK_SECRET}
    networks: [helmdeck_default, openclaw_default]
  2. Set the shared secret in your .env.local:

    HELMDECK_WEBHOOK_SECRET=$(openssl rand -hex 32)
  3. Tell the LLM to use the webhook in your prompt. SKILLS.md teaches the LLM the pattern; for an explicit prompt:

    Render this Marp deck as a narrated video. Use webhook_url=http://helmdeck-callback:8080/done
    and webhook_secret=<your-secret>. Don't poll — I'll get notified when it's ready.

    ---
    marp: true
    ---

    # My Deck
    <!-- Welcome to my deck. -->
    The future is now.
  4. What you'll see: the LLM calls slides.narrate once, gets back "task started" (SEP-1686 envelope), and a few minutes later you see a fresh chat message:

    system: [helmdeck] Pack slides.narrate completed. Video artifact: slides.narrate/<key>/video.mp4 — open at http://localhost:3000/artifacts/...

    The LLM can then respond to that message in its normal turn cycle.

Generic spec

For non-OpenClaw clients (custom A2A bridges, Slack bots, anything else) the same webhook fires with the same payload. See docs/integrations/webhooks.md for the wire contract.

Troubleshooting

  • origin not allowed (use HTTPS or localhost secure context) — OpenClaw's Control UI requires a secure context. Use the SSH tunnel from step 2, not the public IP.
  • OpenClaw can't reach helmdeck-control-plane:3000 — confirm the network overlay is applied: docker network inspect helmdeck_default should list openclaw-gateway as a member.
  • 401 unauthorized on every tool call — JWT expired or wrong scope. Mint a new one and update ~/.openclaw/openclaw.json.
  • tools/list returns nothing — check that the helmdeck Pack Registry is populated: curl -H "Authorization: Bearer $JWT" http://localhost:3000/api/v1/packs should list dozens of packs. If empty, the control plane hasn't registered the built-ins (check docker compose logs control-plane).

References