claude-openmax
A Claude Code runtime adapter for OpenMax/CWS that enables bidirectional messaging and exposes workspace services as MCP tools.
README
claude-openmax
The Claude Code runtime adapter for OpenMax / CWS. A thin Category-B
(bare runtime) adapter: it owns none of the CWS protocol — that all comes from
@openmaxai/openmax-agent-sdk (CwsAgentBridge) — and does
only the two runtime-specific translations plus capability exposure:
- Inbound — bring a workspace message into Claude Code's visible context
via an experimental
claude/channelMCP push (raft-channel-wake.v1). - Outbound — send Claude's reply back to cws-core via the SDK's
CommService. - Capability exposure — the SDK's six service clients
(
tm/kb/as/comm/core/conn) as MCP tools, so the agent can operate the workspace (create issues/tasks, query the KB, upload files, reply).
It follows the proven reference implementation,
raft-external-agents v0.3.1
— the only one of the four external runtimes with a shipped Claude Code plugin —
and the claude-openmax adapter design.
Architecture
Layer 1 @openmaxai/openmax-agent-sdk (CWS HTTP/WS contract only)
CwsAgentBridge: per-org WS lifecycle · auth/heartbeat/reconnect · atomic dedupe
· /sync + inbox-ledger · frame dispatch · access-policy · normalized InboundMessage
services: tm / kb / as / comm / core / conn (one CwsHttpClient)
providers: StorageProvider · RuntimeStateProvider · InboundDelivery(★) · Logger
▲ import + inject
Layer 2 claude-openmax (this repo)
┌ bridge host (Node) ────────────────┐ ┌ Claude Code (agent) ──────────┐
│ new CwsAgentBridge({providers,cbs}) │ │ MCP `openmax` server: │
│ providers.inbound.deliver ─────────┼─wake──▶│ experimental claude/channel │
│ = derive WakeRequest → push │ │ → pushes notice into context │
│ storage=local data dir · logger │ │ MCP tools: tm kb as comm core │
│ holds 6 SDK service clients ◀──────┼─call───┤ conn + comm_send │
└─────────────────────────────────────┘ └───────────────────────────────┘
│ CommService.send() / bridge.send() → cws-core
▼
cws-core REST ◀── cws-comm WS (inbound frames) ── COCO Workspace (user)
Topologies
- In-process (MVP, default) — one Node process is both the stdio MCP server
(
claude/channel+ tools) and the host ofCwsAgentBridge.InboundDeliverypushes wakes straight to the channel; the/wakeHTTP hop is skipped but theWakeRequestwire shape is preserved. Run: Claude Code loads the plugin. - Split (design topology 1,
CLAUDE_OPENMAX_MODE=channel-only) — the MCP plugin runs only the channel + an HTTPPOST /wakeserver; a separate residentbridge.jsholds the WS and POSTs wakes. The bridge survives Claude Code session restarts and redelivers via the SDK's/sync+ inbox-ledger.
The ok:true delivery invariant
The single most important rule (from the SDK's wake-result schema and
CwsAgentBridge): ok:true MUST mean the message genuinely entered the
runtime's visible context. On ok:true the SDK commits dedupe + ledger +
read markers and stops /sync retry for that message — so a false ok:true
loses the message forever.
This adapter returns ok:true only when the wake injection resolved
(ClaudeChannel.notifyWake / POST /wake succeeded). Anything else —
channel not connected, notification write failed, malformed inbound — returns
{ok:false, failureClass, retryAfterMs}, so the SDK holds all markers and
redelivers on the next /sync sweep. See src/inbound-delivery.js and its
tests.
Files
| File | Purpose |
|---|---|
src/index.js |
MCP channel plugin entrypoint (Claude Code loads this over stdio); default in-process bridge host. |
src/bridge.js |
Standalone resident bridge for the split topology; POSTs wakes over HTTP /wake. |
src/channel.js |
MCP Server declaring the experimental claude/channel capability; notifyWake pushes notifications/claude/channel. |
src/wake.js |
Pure raft-channel-wake.v1 derivation + validation + the human-visible wake notice/meta builders. |
src/inbound-delivery.js |
InboundDelivery.deliver() — derive WakeRequest, inject, gate ok:true. |
src/notifier.js |
Debounced wake coalescing (raft EAB-8): leading-edge inject + window merge. |
src/wake-server.js |
HTTP POST /wake server for the split topology (token-guarded). |
src/mcp-tools.js |
Wraps the six SDK service clients as MCP tools (one dispatch tool per service + comm_send). |
src/config.js |
Loads adapter config; builds CwsHttpClient + TokenManager + services; SDK callback seams (session/config/owner persistence). |
src/create-bridge.js |
Assembles CwsAgentBridge from the runtime + providers. |
src/storage.js |
File-backed StorageProvider under a local data dir (XDG); no ~/zylos coupling. |
src/providers.js |
stderr logger + empty RuntimeStateProvider (Cat.B degraded metrics). |
.claude-plugin/plugin.json |
Registers the openmax MCP server for Claude Code. |
hooks/hooks.json + hooks/session-hook.js + hooks/orientation.js |
SessionStart orientation injection (survives resume/compaction). |
CLAUDE.md |
Agent-facing instructions: how wakes arrive, how to read/reply, tool map. |
test/*.test.js |
node --test unit tests (frame derivation, ok:true gating, coalescing, tool dispatch, orientation). |
Session / context management
Uses Claude Code's built-in autocompact (and /clear / /compact). This
adapter implements no extra compression logic — by design.
Installation
Install as a Claude Code plugin from this repo's marketplace:
claude plugin marketplace add openmaxai/claude-openmax # register the marketplace (once)
claude plugin install openmax-channel@openmax # install the plugin
Claude Code fetches the plugin, installs it into its managed plugin directory,
and keeps it updated — no manual git clone needed. Then create your config
(see Configuration) and start Claude Code.
Experimental wake — one caveat. The MCP tools (
tm/kb/as/comm/core/conn+comm_send) work from a plain plugin install with no flags. The inbound wake (claude/channel, how workspace messages reach the agent) is still an experimental Claude Code capability and currently requires launching with the development-channels flag:claude --dangerously-load-development-channels plugin:openmax-channel@openmaxReference the plugin, not a bare server name. When installed via
claude plugin install, Claude Code namespaces this MCP server —/mcpshows it asplugin:openmax-channel:openmax, notopenmax. So the bare--dangerously-load-development-channels server:openmaxdoes not match a plugin install (it only works if you register the server directly in a.mcp.jsonunder the literal nameopenmax); use theplugin:…@…form above. If in doubt, run/mcpand reference the exact server name it prints. Onceclaude/channelgraduates from experimental, install alone will be enough.
Running (from source / dev)
npm install # resolves @openmaxai/openmax-agent-sdk@alpha from npm
cp config.example.json ~/.config/claude-openmax/config.json # fill in real values
npm test # node --test
Load into Claude Code as a plugin (dev):
claude plugin marketplace add --scope local /path/to/claude-openmax # local checkout
# or point Claude Code at .claude-plugin/plugin.json directly
Build (maintainers)
The plugin ships a dependency-free bundle. Claude Code installs a marketplace
plugin by cloning the repo and does not run npm install, so the MCP server
must run with zero node_modules. scripts/build.js (esbuild) inlines every
dependency into dist/index.mjs (the MCP server, referenced by
.claude-plugin/plugin.json) and dist/bridge.mjs (the split-topology bridge).
npm run build # rebuild dist/ after changing src/ or bumping a dependency
dist/ is committed (it is the shipped artifact); CI rebuilds it and fails if the
committed bundle is stale, and smoke-tests that it loads with no node_modules.
Split topology (resident bridge + channel-only plugin):
# terminal A: Claude Code loads the plugin with
CLAUDE_OPENMAX_MODE=channel-only CLAUDE_OPENMAX_WAKE_PORT=47600 CLAUDE_OPENMAX_WAKE_TOKEN=... claude ...
# terminal B: resident bridge (config.wake.endpoint = http://127.0.0.1:47600/wake)
CLAUDE_OPENMAX_WAKE_TOKEN=... node src/bridge.js
Config / env
Config file at $CLAUDE_OPENMAX_CONFIG (or ~/.config/claude-openmax/config.json);
see config.example.json. As of the config-parity refactor the on-disk shape is a
1:1 structural mirror of the OpenMax (zylos-openmax) component's config — see
the migration note below. The shape:
enabled?: bool
server: { bff_url, ws_url, frontend_base_path } // frontend_base_path default "/workspace"
agent: { identity_id, api_key, device_id, app_version }
cf_access: { client_id, client_secret }
orgs: { "<org_id>": { enabled?, org_id, org_name?,
owner: { member_id, name },
self: { member_id, name, display_name },
access:{ dmPolicy, dmAllowFrom?, groupPolicy?, groups?:{ "<convId>": { mode, allowFrom } } } } }
wake: { endpoint } // claude-openmax ONLY (openmax has no wake)
metricsReport?: { dashboardApiKey } // RESERVED / forward-compat — inert (no reporter yet)
ws?: { reconnectMaxMs?, heartbeatIntervalMs?, pingIntervalMs? } // claude-openmax WS tuning knobs
Env fallbacks (map onto the nested fields): COCO_API_URL→server.bff_url,
COCO_WS_URL→server.ws_url, COCO_FRONTEND_BASE_PATH→server.frontend_base_path,
COCO_API_KEY→agent.api_key, COCO_DEVICE_ID→agent.device_id,
COCO_CLIENT_VERSION→agent.app_version, COCO_ORG_ID→default org. Other knobs:
CLAUDE_OPENMAX_DATA_DIR, CLAUDE_OPENMAX_MODE, CLAUDE_OPENMAX_DEBOUNCE_MS,
CLAUDE_OPENMAX_CONTENT_FREE, CLAUDE_OPENMAX_WAKE_{HOST,PORT,TOKEN}.
orgs is keyed by org_id (openmax-style), end to end: the SDK orchestrator
keys its per-org runtime records by org_id too, so the adapter hands it an
org_id-keyed map directly — there is no separate per-org key to derive. Every
self-healing write-back (self.member_id, self.name, owner bind) resolves the
org by org_id and lands back in the org_id-keyed on-disk structure.
agent.identity_id is the agent's global identity. Leave it empty and the
adapter resolves it from cws-core GET /me at startup and caches it back to
config.json. It is the leadAgentId for the guided-autonomy flow (an Issue's
Lead agent = the agent itself).
server.frontend_base_path is wired into the SDK's CwsHttpClient.frontendUrl()
so the agent can build clickable workspace links (<bff_url><frontend_base_path>/…,
default /workspace).
Migrating from the openmax (zylos-openmax) component
The claude-openmax config is now structurally identical to the openmax
component's config.json — you can drop an openmax config in as-is. The only
differences are additive and claude-openmax-specific:
wake.endpoint— required for the split-topology bridge; openmax has no wake block.metricsReport— accepted for parity but inert (claude-openmax has no metrics reporter yet); it round-trips untouched.ws— optional WS tuning knobs (reconnectMaxMs,heartbeatIntervalMs,pingIntervalMs) that openmax hardcodes;ws_url/device_id/app_versionlive underserver.*/agent.*, NOT here.
The old claude-openmax shape (top-level http/auth + an array orgs) is
still accepted: it is translated to the new shape on load with a one-time warning,
so an existing live config won't break — but you should migrate it.
Session files auto-migrate. Per-org state (incl. the /sync cursor) is now
keyed by org_id (sessions/<org_id>.json), where earlier builds used a derived
slug (sessions/<slug>.json). On first load, if only a legacy sessions/<slug>.json
exists (matching an explicit slug or slugify(org_name)), it is copied forward
to the org_id key so the cursor is preserved — no duplicate message delivery after
upgrade. The old file is left in place (harmless); no manual step is needed.
Each org also honors an enabled: false flag — such orgs are kept on disk but
not connected to (parity with the openmax component).
Verified vs. spike (honesty ledger)
Verified locally (node --test + MCP client smoke):
- WakeRequest derivation/validation,
ok:truegating, coalescing, tool dispatch — 32 unit tests green. - The MCP server boots, advertises
capabilities.experimental["claude/channel"], and lists all 7 tools to a real MCP client. POST /wake(token-guarded) →server.notification({method:"notifications/claude/channel", ...})is actually transmitted over the MCP transport and received by the connected client with correct content + routing meta, and the server returns{ok:true, runtimeSession}.server.notificationwith the customclaude/channelmethod does not throw on@modelcontextprotocol/sdk1.22+.
⚠️ SPIKE — requires a live Claude Code to confirm (biggest technical uncertainty):
claude/channelrendering. We proved the notification reaches an MCP client; we have not proved Claude Code (a) enables this experimental capability, (b) renders the pushed notice into the agent's visible context, and (c) does so promptly / can steer an in-progress turn. Until confirmed,ok:truemeans "notification written to the MCP transport", which is the strongest local signal but weaker than "the model has seen it". If Claude Code proves fire-and-forget here, we should fall back to a more conservativeok:truegate (and lean harder on/sync).runtimeSessionbinding. We mint a stable per-process id; the canonical value is Claude Code's real session id, which must be sourced from the live runtime.- Session lifecycle / auth jitter under the in-process topology (WS drops when the Claude Code session exits) needs a live soak test; the split topology is the mitigation and also needs end-to-end restart verification.
- Tool budget. We collapsed ~150 sub-commands into 6 dispatch tools +
comm_sendto stay within Claude Code's tool budget; the exact budget and whether dispatch-style tools are ergonomic for the model is unconfirmed.
Boundaries
- Consumes the SDK for all protocol/transport/sync/dedup/access-policy logic;
reimplements none of it. Does not pass a custom
callbacks.dedupe(uses the SDK's built-in atomic deduper). - No dependency on
zylos-openmax; independent local data dir (no~/zylos).
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.