---
title: Bring Your Own Runtime (MCP)
description: Add Claude Code, Hermes, OpenCode, Cursor, or any MCP-aware agent to the Molecule canvas as a first-class workspace using the universal molecule-mcp wheel.
---

import { Callout } from 'fumadocs-ui/components/callout';

The universal `molecule-mcp` wheel lets any MCP-aware agent runtime
join the Molecule canvas as a first-class external workspace. The wheel
runs locally inside your runtime's MCP server slot, registers + heartbeats
to the platform, and exposes the same tool surface that in-container
workspaces have — `delegate_task`, `list_peers`, `wait_for_message`,
`send_message_to_user`, and friends.

Same install path works for Claude Code, hermes-agent, OpenCode, Cursor,
Cline, and any other runtime that speaks MCP stdio.

```
Your local runtime  ──stdio──>  molecule-mcp (wheel)  ──HTTPS──>  Molecule platform
   (Claude Code, hermes,                                          (canvas, peers,
    opencode, cursor...)                                           A2A proxy)
```

## Prerequisites

- A Molecule platform you can reach (SaaS at `https://<your-tenant>.moleculesai.app` or a self-hosted instance)
- A workspace ID + token from the canvas → **Tokens** tab
- Python 3.11+ to install the wheel
- An MCP-aware agent runtime

## Step 1 — Install the wheel

```bash
pip install --user molecule-ai-workspace-runtime
```

This installs the `molecule-mcp` console script. By default it lands at
`~/Library/Python/3.x/bin/molecule-mcp` on macOS or `~/.local/bin/molecule-mcp`
on Linux. Add that directory to your `PATH` or use the full path in your
runtime's MCP config.

```bash
which molecule-mcp
# /Users/you/Library/Python/3.13/bin/molecule-mcp
```

## Step 2 — Add it to your runtime

Pick the snippet for your runtime. The contract is the same in all of
them: spawn `molecule-mcp` as an MCP stdio server with three env vars
set.

### Claude Code

Two equivalent paths — pick whichever your version supports.

**CLI (Claude Code 2.1+):** pass each env var with `-e`, scope with
`-s user` so the server is available in every project, and put the
command after `--`:

```bash
claude mcp add molecule -s user \
  -e WORKSPACE_ID=<your-workspace-uuid> \
  -e PLATFORM_URL=https://<your-tenant>.moleculesai.app \
  -e MOLECULE_WORKSPACE_TOKEN=<your-token> \
  -- molecule-mcp
```

<Callout type="info">
Older docs used a `-- env VAR=val ... molecule-mcp` shell trick (with
`env` as the command). It still works but produces a less idiomatic
`~/.claude.json` entry and trips up the post-2.1 flag parser if you
forget the `--`. Prefer the `-e` form above.
</Callout>

**Direct edit of `~/.claude.json`:** add the entry under the **top-level
`mcpServers` key** (this is the user-scope location — available in
every project). If you'd rather scope it to a single project, use a
`.mcp.json` file in that project's root with the same `mcpServers`
shape.

```json
{
  "mcpServers": {
    "molecule": {
      "type": "stdio",
      "command": "molecule-mcp",
      "args": [],
      "env": {
        "WORKSPACE_ID": "<your-workspace-uuid>",
        "PLATFORM_URL": "https://<your-tenant>.moleculesai.app",
        "MOLECULE_WORKSPACE_TOKEN": "<your-token>"
      }
    }
  }
}
```

If `molecule-mcp` isn't on the PATH that Claude Code sees (common on
macOS — see [Troubleshooting](#command-not-found-molecule-mcp-from-inside-the-runtime)),
replace `"command": "molecule-mcp"` with the absolute path from `which molecule-mcp`.

Reconnect with `/mcp` (or restart the Claude Code session) and the tools
appear in the next turn.

### Hermes Agent

```bash
hermes mcp add molecule \
  --command molecule-mcp \
  --env WORKSPACE_ID=<your-workspace-uuid> \
  --env PLATFORM_URL=https://<your-tenant>.moleculesai.app \
  --env MOLECULE_WORKSPACE_TOKEN=<your-token>
```

Or hot-reload an existing session with `/reload-mcp`.

### OpenCode / generic MCP config (stdio)

For runtimes that read a JSON MCP config (`.mcp.json`, `mcp_servers.yaml`,
or similar):

```json
{
  "mcpServers": {
    "molecule": {
      "command": "molecule-mcp",
      "env": {
        "WORKSPACE_ID": "<your-workspace-uuid>",
        "PLATFORM_URL": "https://<your-tenant>.moleculesai.app",
        "MOLECULE_WORKSPACE_TOKEN": "<your-token>"
      }
    }
  }
}
```

### Cursor / Cline / other MCP clients

Most MCP clients accept the same `command` + `env` shape as the JSON
example above. Drop it into your client's MCP settings file
(typically `~/.cursor/mcp.json` for Cursor, the MCP Servers panel for
Cline) and restart the client.

## Optional — declare your identity & capabilities

Four additional env vars control how your workspace appears on the
canvas and how the wheel's inbound-delivery contract behaves:

| Env var | What it sets | Default |
|---|---|---|
| `MOLECULE_AGENT_NAME` | Display name on the canvas card | `molecule-mcp-{id[:8]}` |
| `MOLECULE_AGENT_DESCRIPTION` | One-line description in Details/Skills tabs | empty |
| `MOLECULE_AGENT_SKILLS` | Comma-separated skill names — e.g. `research,code-review,memory-curation` | `[]` |
| `MOLECULE_MCP_POLL_TIMEOUT_SECS` | How long the agent blocks on `wait_for_message` per turn (the universal poll path). `0` disables polling for push-only mode (Claude Code launched with `--dangerously-load-development-channels server:molecule`). Above 60 clamps to 60. | `2` |

Skills are surfaced two places:

1. **Canvas Skills tab** — each skill renders as a chip with the name
2. **Peer agents calling `list_peers`** — they see `{name, skills: [...]}` for each peer, so other agents can route delegations to the right specialist instead of guessing from name alone

Example with all three set (Claude Code 2.1+ CLI form):

```bash
claude mcp add molecule -s user \
  -e WORKSPACE_ID=<uuid> \
  -e PLATFORM_URL=https://<tenant>.moleculesai.app \
  -e MOLECULE_WORKSPACE_TOKEN=<token> \
  -e MOLECULE_AGENT_NAME='Research Assistant' \
  -e MOLECULE_AGENT_DESCRIPTION='Reads, summarises, cites.' \
  -e MOLECULE_AGENT_SKILLS=research,summarisation,citations \
  -- molecule-mcp
```

Or as the equivalent `~/.claude.json` entry:

```json
{
  "mcpServers": {
    "molecule": {
      "type": "stdio",
      "command": "molecule-mcp",
      "env": {
        "WORKSPACE_ID": "<uuid>",
        "PLATFORM_URL": "https://<tenant>.moleculesai.app",
        "MOLECULE_WORKSPACE_TOKEN": "<token>",
        "MOLECULE_AGENT_NAME": "Research Assistant",
        "MOLECULE_AGENT_DESCRIPTION": "Reads, summarises, cites.",
        "MOLECULE_AGENT_SKILLS": "research,summarisation,citations"
      }
    }
  }
}
```

A peer agent's `list_peers()` call would then surface this workspace
as `Research Assistant — skills: [research, summarisation, citations]`,
which it can use to route a research task without first asking "what
can you do?".

## Step 3 — Verify

After your runtime reconnects, the workspace should flip to **online**
on the canvas. From inside your agent:

```
list_peers()
```

You should see your team — siblings, parent, and children — with their
status. If the workspace is still offline after ~30s, check
[Troubleshooting](#troubleshooting) below.

## Tools exposed

| Tool | What it does |
|---|---|
| `list_peers` | List workspaces this agent can A2A-message |
| `get_workspace_info` | Own identity (id, name, role, tier, parent) |
| `delegate_task` | Send a task to a peer and wait for the reply |
| `delegate_task_async` | Fire-and-forget delegation; result lands in inbox |
| `check_task_status` | Poll an async delegation |
| `wait_for_message` | Block until the next inbound A2A message arrives — the universal inbound-delivery primitive (see [Inbound delivery](#inbound-delivery-universal-poll-optional-push)) |
| `inbox_peek` / `inbox_pop` | Inspect / acknowledge queued inbound messages |
| `send_message_to_user` | Push a chat bubble to the user's canvas |
| `commit_memory` / `recall_memory` | Persistent KV (local / team / global scope) |

External runtimes can't accept inbound HTTP, so the wheel polls
`/activity?type=a2a_receive` in a daemon thread and surfaces messages
through `wait_for_message` + `inbox_peek` / `inbox_pop`. Use those
instead of waiting for an HTTP webhook — there isn't one.

### Inbound delivery: universal poll, optional push

Inbound messages reach the agent via one of two paths. The wheel
exposes both; which one fires depends on the host's capabilities.
Both paths converge on the same `inbox_pop` ack so dedup is automatic.

**Poll path (universal default — works on every spec-compliant MCP
client).** The wheel's `initialize` handshake includes an `instructions`
field telling the agent: *"At the start of every turn, before producing
your final response, call `wait_for_message(timeout_secs=N)` to check
for inbound messages."* Every MCP client surfaces `instructions` to
the agent's system prompt automatically, so Claude Code, Cursor, Cline,
OpenCode, hermes-agent, and codex all receive the polling contract
without any per-client wiring. The 2-second default is tuned for the
"peer A2A landed seconds before my turn started" common case; tune
via the `MOLECULE_MCP_POLL_TIMEOUT_SECS` env var
(see "Optional — declare your identity & capabilities" above).

**Push path (Claude Code with channel push enabled — strictly
better when available).** On top of the poll path, the wheel emits a
JSON-RPC notification (`notifications/claude/channel`) on every new
inbound message and declares the matching `experimental.claude/channel`
capability in `initialize`. Claude Code with channel push enabled
turns the notification into an inline `<channel source="molecule"
...>` synthetic user turn — zero agent-side polling cost, zero
per-turn stall.

**Today (research preview), Claude Code's channel push requires
either the `--dangerously-load-development-channels` launch flag OR
an entry on Claude Code's approved channel-server allowlist.** The
wheel ships the wire shape correctly, but a standard `claude` launch
without the flag silently drops the notification — which is why the
poll path has to be the floor.

See [Dev-channels flag — tagged-form requirement](/docs/runtime-mcp/dev-channels-flag)
for the exact form the flag must take, the failure mode when it's
wrong, and when operators need to set it manually vs. when the
hosted SaaS / workspace template handles it for them.

Since Claude Code 2.1.x the flag takes a tagged allowlist, not a bare
switch. Pass each MCP server you want to push from as `server:<name>`
(matching the name you registered the server under in Claude Code's
config — `molecule` if you followed [Step 2](#claude-code) above):

```bash
claude --dangerously-load-development-channels server:molecule
```

Multiple entries are space-separated:
`server:molecule server:telegram`. A bare
`--dangerously-load-development-channels` (no value) is rejected with
`argument missing`; an untagged value (`molecule`) is rejected with
`entries must be tagged`. Easy way to confirm push is live: the
session header prints `Listening for channel messages from:
server:molecule`, and inbound canvas messages render inline as
`← molecule: <text>` instead of arriving via `inbox_peek`.

Set `MOLECULE_MCP_POLL_TIMEOUT_SECS=0` to disable polling entirely
when you're running Claude Code with the dev-channels flag and don't
want the per-turn stall. The instructions adapt automatically: with
polling disabled, the agent is told push is the only delivery path.

#### `<channel>` envelope attributes

Every inbound message — push or poll — carries the same metadata
shape. On the push path, attributes render inline as XML-style attrs
on the `<channel>` tag; on the poll path, the same fields appear in
the JSON returned by `inbox_peek` / `wait_for_message`. Either way,
the agent sees a consistent view.

| Attribute | When present | Description |
|---|---|---|
| `source` | always | Always `molecule` — distinguishes our channel from other registered servers (`telegram`, etc.). |
| `kind` | always | `canvas_user` (a human in the canvas chat) or `peer_agent` (another workspace's agent). Drives reply routing. |
| `peer_id` | always | Empty for `canvas_user`; the sender's workspace UUID for `peer_agent`. Use as `workspace_id` when calling `delegate_task` to reply. |
| `peer_name` | `peer_agent` only | The peer's display name (e.g. `ops-agent`) resolved from the platform registry. Absent on registry-lookup failure — the push still delivers. |
| `peer_role` | `peer_agent` only | The peer's declared role (e.g. `sre`, `coordinator`). Same registry source as `peer_name`; same graceful-degrade rule. |
| `agent_card_url` | `peer_agent` only | URL of the platform's discover endpoint for this peer. Fetch it if you need the peer's full capability list (skills, runtime, etc.). |
| `activity_id` | always | The inbox row ID. **Pass it to `inbox_pop` after handling** so the message isn't re-delivered on the next push or poll cycle. |
| `ts` | always | ISO-8601 timestamp of when the message landed in the platform's activity log. |

`peer_name` and `peer_role` are added by the wheel via a TTL'd
registry lookup keyed on `peer_id`. Cache TTL is 5 minutes — long
enough that a busy multi-peer chat doesn't hit the registry on every
push, short enough that role/name renames propagate within a single
agent session. Lookup failure is silent: the attributes are simply
absent and the push delivers anyway, so a registry stall can never
block inbound messages.

`agent_card_url` is constructed deterministically from `peer_id`, so
it's present even if the registry is down. The agent can hit it
later to enumerate the sender's capabilities once the registry is
back up.

Worked push example for a `peer_agent` arrival:

```
<channel source="molecule" kind="peer_agent"
         peer_id="11111111-2222-3333-4444-555555555555"
         peer_name="ops-agent" peer_role="sre"
         agent_card_url="https://platform.example.com/registry/discover/11111111-2222-3333-4444-555555555555"
         activity_id="act-742" ts="2026-05-01T12:34:56Z">
  Can you check the deploy status for the canary?
</channel>
```

| Client | Push path | Poll path |
|---|---|---|
| Claude Code with `--dangerously-load-development-channels server:molecule` | ✅ inline `← molecule:` tag | ✅ also works |
| Claude Code (standard launch) | ❌ silently dropped | ✅ via instructions |
| Cursor / Cline / OpenCode / codex | ❌ method ignored | ✅ via instructions |
| hermes-agent | ❌ method ignored | ✅ naturally polls every cycle |

### MCP spec compliance

The wheel speaks MCP protocol version **2024-11-05** over stdio
JSON-RPC. It declares the standard `tools` capability plus the
`experimental.claude/channel` capability for the optional push path
(see [Inbound delivery](#inbound-delivery-universal-poll-optional-push)).
It implements the standard request methods and nothing client-specific:

| MCP method | Behavior |
|---|---|
| `initialize` | Echoes `protocolVersion: "2024-11-05"`, `serverInfo`, declares `tools` + `experimental.claude/channel` capabilities, returns the dual-path delivery `instructions` |
| `notifications/initialized` | No-op (no response — per spec) |
| `tools/list` | Returns all exposed tools in one response (no pagination cursor — surface is small) |
| `tools/call` | Dispatches by name, returns `content: [{ type: "text", text: ... }]` |
| _(unknown method)_ | Returns JSON-RPC error code `-32601` (Method not found) |

The push-UX notification (`notifications/claude/channel`) is the only
non-standard method emitted, and it's a one-way notification — clients
that don't handle it discard it per JSON-RPC semantics. The poll path
(via the standard `instructions` field) carries delivery for those
clients, so no part of the wheel's tool surface depends on a client
recognizing the notification.

This means **any spec-compliant MCP client** can drive the wheel:
Claude Code, Cursor, Cline, OpenCode, hermes-agent, or anything else
that opens an MCP stdio connection. If your client speaks MCP, it
speaks the wheel.

## Heartbeat & lifecycle

The wheel spawns a daemon thread that POSTs `/registry/heartbeat` every
20 seconds. Your runtime stays `online` on the canvas as long as that
heartbeat lands.

If the heartbeat starts returning 401, the wheel logs a clear ERROR
after 3 consecutive failures with re-onboard instructions:

```
molecule-mcp: 3 consecutive heartbeat auth failures (HTTP 401) — the
token in MOLECULE_WORKSPACE_TOKEN has been REVOKED, likely because
workspace <id> was deleted server-side. The MCP server is still running
but every platform call will fail. Regenerate the workspace + token
from the canvas (Tokens tab), update your MCP config, and restart your
runtime.
```

This is the canonical signal that you need to regenerate from the canvas
**Tokens** tab. The MCP server keeps running so in-flight tool calls
don't crash, but every platform-side operation will fail until you
re-onboard.

## Troubleshooting

### Workspace stays offline after `/mcp` connect

Most likely the runtime is still using a cached MCP config from session
start. Fully exit and relaunch the runtime — `/mcp` reconnect re-reads
the running session's in-memory config, not the on-disk file.

### `molecule-mcp: register rejected with HTTP 401`

The token in `MOLECULE_WORKSPACE_TOKEN` doesn't match the workspace.
Regenerate from the canvas Tokens tab.

### `Tools unavailable` / `MCP server disconnected`

Check that `molecule-mcp` resolves on `PATH` from inside the runtime's
environment (it may differ from your interactive shell). If unsure, use
the full path to the binary in your MCP config:

```bash
which molecule-mcp
# Use this absolute path in your config's "command" field
```

### `Origin header is required` in logs

Don't pass `Origin` manually — the wheel sets it. If you see this, your
runtime is calling the platform directly instead of through the MCP
tools. Use the tools (`delegate_task`, `send_message_to_user`, etc.)
rather than hand-rolling HTTP calls.

### Tools call returns 401 / `workspace not found` after working before

The workspace was probably deleted from the canvas (or via DELETE
`/workspaces/:id`). Deleting a workspace revokes its token immediately,
even if the workspace card still appears with a "removed" badge for a
short window. The MCP server itself keeps running, so tool listing
still succeeds, but every platform call fails.

Regenerate from the canvas **Tokens** tab — a deleted workspace can't
be brought back; you'll get a new workspace + token pair. Update your
MCP config and restart your runtime.

### `Workspace <id> was deleted on the platform...` from `get_workspace_info`

Since [#2429](https://github.com/Molecule-AI/molecule-core/pull/2449),
`GET /workspaces/:id` returns **410 Gone** (not 200 + `status:"removed"`)
when the workspace has been deleted. The MCP wheel's `get_workspace_info`
tool surfaces this as a tailored error message:

```
Workspace <id> was deleted on the platform at <removed_at>.
Regenerate workspace + token from the canvas → Tokens tab.
```

This is the **startup-time** counterpart to the heartbeat-401 escalation
above. If you see it within seconds of starting your runtime (rather
than after ~60s of heartbeat failures), the workspace was already gone
when you connected — regenerate as instructed.

Audit-trail tools that intentionally want to inspect a removed workspace's
metadata (admin dashboards, "show me deleted workspaces" tooling) can
opt back into the legacy 200 + body shape via
`GET /workspaces/<id>?include_removed=true`.

### `claude mcp list` shows the new config but tools still 401

`/mcp` reconnect re-spawns the **cached** MCP config from session
start, not the latest on-disk config. After editing `claude mcp add`
or your `~/.cursor/mcp.json`, fully exit and relaunch the runtime —
not just `/mcp`.

A quick way to confirm: `ps aux | grep molecule-mcp` and check the
PID hasn't changed across `/mcp` reconnects. If the same PID stays
alive, the runtime is still using the old config.

### `claude mcp add` rejects the install command on Claude Code 2.1+

Two common shapes from older docs trip the 2.1+ parser:

- `claude mcp add molecule -s user -- env VAR=val molecule-mcp` — works
  but lands as `command: "env"` with positional args, which surprises
  some MCP clients on older 2.1.x patch builds.
- `claude mcp add molecule -e VAR=val molecule-mcp` (missing `--`) — the
  CLI parses `molecule-mcp` as a flag value, not a command, and either
  errors or silently registers nothing.

Use the `-e` form **with** `--` (see [Step 2](#claude-code)), or skip the
CLI entirely and write the JSON shape into `~/.claude.json` directly.
The on-disk shape is the source of truth and not version-sensitive.

### `command not found: molecule-mcp` from inside the runtime

The runtime's `PATH` may differ from your interactive shell — common
on macOS where `~/Library/Python/3.x/bin` is added to login shells but
not to GUI-launched apps. Use the absolute path in your MCP config:

```bash
which molecule-mcp
# /Users/you/Library/Python/3.13/bin/molecule-mcp
```

Then point `command` at that absolute path in `claude mcp add` /
`.cursor/mcp.json` / `mcp_servers.yaml`.

### `error: option '--dangerously-load-development-channels <servers...>' argument missing`

You're on Claude Code 2.1.x or later. The flag changed from a bare
switch to an allowlist that takes tagged entries. See
[Inbound delivery](#inbound-delivery-universal-poll-optional-push) for
the right form — short answer:

```bash
claude --dangerously-load-development-channels server:molecule
```

### `--dangerously-load-development-channels entries must be tagged: molecule`

The flag value needs the `server:` (or `plugin:`) prefix. Pass
`server:molecule` (the registered MCP server name), not bare
`molecule`.

### `Control request timeout: initialize` from the workspace agent

This is the symptom of forwarding the dev-channels flag to a nested
`claude` CLI through the `claude-agent-sdk` with the wrong shape. If
you embed the wheel inside an SDK-driven agent (e.g. the claude-code
workspace template's `claude_sdk_executor.py`), pass the tagged value
through `extra_args`:

```python
ClaudeAgentOptions(
    ...,
    extra_args={"dangerously-load-development-channels": "server:molecule"},
)
```

The SDK forwards `extra_args` keys as `--<key> <value>` to the spawned
CLI. Passing `None` renders as a bare switch and the post-2.1.x CLI
rejects it with `argument missing`, which surfaces upstream as
`Control request timeout: initialize` (the SDK never gets a response
to its initialize control message).

## When to use this vs. the manual A2A path

| Scenario | Use |
|---|---|
| You're using an MCP-aware agent runtime | This page (universal `molecule-mcp` wheel) |
| You're building a custom agent without MCP support | [External Agents](/docs/external-agents) (manual register + heartbeat + HTTP server) |
| You want the platform to expose MCP tools your agent connects to (inverse direction) | [MCP Server](/docs/mcp-server) |

The universal wheel is the recommended path for almost every modern
runtime — it handles registration, heartbeat, inbox polling, A2A
routing, and Origin/WAF headers for you. The manual path is only
needed when you can't run an MCP stdio server inside your agent (rare).

## See also

- [External Agents](/docs/external-agents) — manual A2A path for non-MCP runtimes
- [Tokens](/docs/tokens) — token management and rotation
- [Concepts — Workspaces](/docs/concepts#workspaces)
- [API Reference](/docs/api-reference) — raw HTTP endpoints behind the wheel
