Compare commits

..

2 Commits

Author SHA1 Message Date
technical-writer a3ad08e8ab [technical-writer-agent] fix(guides): migrate github.com refs to Gitea in Claude Code Channel guide
Secret scan / secret-scan (pull_request) Failing after 5s
CI / build (pull_request) Successful in 37s
Post-suspension URL migration — all Molecule-AI/molecule-mcp-claude-channel
references now point to git.moleculesai.app/molecule-ai/.

Kept github.com for anthropics/claude-plugins-official (external project).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 07:53:56 +00:00
technical-writer 97fbdfb74f [technical-writer-agent]
Secret scan / secret-scan (pull_request) Failing after 29s
CI / build (pull_request) Successful in 4m32s
docs(guides): add Claude Code Channel Plugin guide

New guide for connecting a Claude Code laptop session to Molecule AI
as an external workspace via MCP polling.

No tunnel required, no public URL needed. Polling-based inbound
with direct A2A outbound.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 06:48:45 +00:00
2 changed files with 222 additions and 214 deletions
@@ -0,0 +1,222 @@
---
title: "Claude Code Channel Plugin — Connect a Claude Code Session as an External Workspace"
description: "Bridge Molecule A2A traffic into a running Claude Code session via MCP. Polling-based, no tunnel required. The fastest path for laptop-launched Claude Code sessions to participate in your Molecule canvas."
---
# Claude Code Channel Plugin
Run [Claude Code](https://claude.com/claude-code) on your laptop and have it appear on the Molecule AI canvas as a first-class external workspace. Inbound A2A messages from peer workspaces surface as conversation turns; replies route back through Molecule's A2A endpoints.
> **What this is:** [`molecule-mcp-claude-channel`](https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel) — an MCP-based "channel plugin" that turns a Claude Code session into a Molecule workspace.
> **What this is NOT:** the [Python SDK / curl register flow](/docs/guides/external-agent-registration) for arbitrary HTTP-speaking agents. That flow needs a public URL the platform can POST to. This one polls — runs on any laptop behind any NAT.
---
## What you get
```
Molecule peer ──A2A──▶ [your workspace] ──poll──▶ [plugin] ──MCP notification──▶ Claude Code
▲ │
└────── POST /workspaces/:id/a2a ◄── reply_to_workspace ──┘
```
| Property | Value |
|---|---|
| **Inbound latency** | up to `MOLECULE_POLL_INTERVAL_MS` (default 5s) |
| **Outbound latency** | direct POST — sub-second |
| **Tunnel / public URL** | not required |
| **Auth model** | per-workspace bearer token (same as Python SDK) |
| **Multi-workspace** | yes, comma-separated list |
---
## Prerequisites
| You need | Notes |
|---|---|
| A Molecule AI tenant | Self-hosted localhost or your `*.staging.moleculesai.app` SaaS tenant |
| One or more workspace IDs | Created via canvas or `POST /workspaces` (see [External Agent Registration](/docs/guides/external-agent-registration)) |
| The workspace bearer token | Shown once when the workspace is created — save it from the canvas modal |
| Claude Code | `claude` CLI ≥ the version that supports `--channels` |
| `bun` | The plugin runs under bun for fast startup; `bun install` is invoked automatically by `start` |
> **Note:** The platform must be running molecule-core ≥ PR #2300, which shipped the `?since_secs=` query parameter on `GET /workspaces/:id/activity`. Available on all staging-onward and self-hosted main builds after 2026-04-29.
---
## Step 1 — Create the workspace
In your Molecule canvas:
1. Click **+ New workspace**
2. Choose **External** runtime
3. Set tier as needed; click **Create**
4. The "Connect your external agent" modal opens — switch to the **Claude Code** tab
5. Copy the entire snippet (everything from the `mkdir -p` line through `claude --channels ...`)
Or via API:
```bash
curl -X POST "$MOLECULE_PLATFORM_URL/workspaces" \
-H "Content-Type: application/json" \
-d '{"name": "My Claude Code", "external": true, "tier": 2}'
```
The response includes `claude_code_channel_snippet` — same content as the canvas tab, ready to paste.
## Step 2 — Set up the channel config
Run the snippet from Step 1. It does two things:
```bash
mkdir -p ~/.claude/channels/molecule
cat > ~/.claude/channels/molecule/.env <<'EOF'
MOLECULE_PLATFORM_URL=https://your-tenant.staging.moleculesai.app
MOLECULE_WORKSPACE_IDS=ws-uuid-1
MOLECULE_WORKSPACE_TOKENS=<paste auth_token from create response>
EOF
chmod 600 ~/.claude/channels/molecule/.env
```
Replace the token placeholder with the workspace bearer from Step 1.
## Step 3 — Launch Claude Code
```bash
claude --channels plugin:molecule@molecule-ai/molecule-mcp-claude-channel
```
You should see on stderr (use `--debug` to surface):
```
molecule channel: connected — watching 1 workspace(s) at https://your-tenant.staging.moleculesai.app
workspaces: ws-uuid-1
poll: every 5000ms with 30s window
```
That's it — the workspace is live on the canvas with a purple **REMOTE** badge, and any A2A traffic the workspace receives surfaces as conversation turns in your Claude Code session.
---
## How replies work
When a peer's message lands in your session, you'll see a turn with structured metadata:
```json
{
"method": "notifications/claude/channel",
"params": {
"content": "Hey, can you take a look at this? <issue body>",
"meta": {
"source": "molecule",
"workspace_id": "ws-uuid-1",
"peer_id": "ws-uuid-pm-coordinator",
"method": "user_message",
"activity_id": "act-...",
"ts": "2026-04-29T..."
}
}
}
```
Reply normally — Claude calls the `reply_to_workspace` MCP tool with `peer_id` from the meta block, and the response flows back through `POST /workspaces/:peer_id/a2a` so peers see it just like any other A2A message.
---
## Multi-workspace setup
Watch multiple workspaces from a single Claude Code session by comma-separating the lists. Both must have the same length and order:
```bash
MOLECULE_WORKSPACE_IDS=ws-pm,ws-researcher,ws-engineer
MOLECULE_WORKSPACE_TOKENS=tok-pm,tok-researcher,tok-engineer
```
When Claude replies, the `reply_to_workspace` tool requires `workspace_id` (which of the watched workspaces to reply AS) explicitly. With a single workspace it's implicit.
---
## Configuration reference
| Variable | Default | Purpose |
|---|---|---|
| `MOLECULE_PLATFORM_URL` | (required) | Tenant base URL (no trailing slash) |
| `MOLECULE_WORKSPACE_IDS` | (required) | Comma-separated workspace UUIDs to watch |
| `MOLECULE_WORKSPACE_TOKENS` | (required) | Comma-separated bearer tokens, **same order as IDs** |
| `MOLECULE_POLL_INTERVAL_MS` | `5000` | How often each workspace is polled (ms) |
| `MOLECULE_POLL_WINDOW_SECS` | `30` | `since_secs` window per poll. Wider than interval to recover from missed ticks |
| `MOLECULE_STATE_DIR` | `~/.claude/channels/molecule` | Override state directory (testing) |
---
## Architecture notes
### Why polling instead of push?
The [Python SDK external-agent flow](/docs/guides/external-agent-registration) uses **push**: register an inbound URL, platform POSTs A2A to that URL. Lower latency but requires a tunnel (ngrok / Cloudflare) or static IP — non-trivial for laptop sessions.
This plugin uses **polling** as the default because it works through every NAT/firewall with zero infra. Cost: up to `MOLECULE_POLL_INTERVAL_MS` of inbound latency. For production setups where lower latency matters, push mode is on the v0.2 roadmap.
### Why the 30s window over a 5s interval?
A single missed tick (transient network blip, GC pause, laptop sleep) shouldn't lose messages. The plugin re-fetches the last 30 seconds on every poll and dedups by `activity_id`, so 25 seconds of overlap is the recovery margin. Increase `MOLECULE_POLL_WINDOW_SECS` for noisier networks.
### Singleton lock
Only one channel server runs per host — multiple instances would race the dedup state and double-deliver. The plugin maintains a PID file at `~/.claude/channels/molecule/bot.pid` and on startup kills any stale predecessor. This mirrors the [`@claude-plugins-official/telegram`](https://github.com/anthropics/claude-plugins-official/tree/main/plugins/telegram) pattern.
---
## Troubleshooting
### "molecule channel: required config missing"
The plugin started before you filled in `.env`. Re-run the snippet from Step 2, then re-launch Claude Code.
### "molecule channel: poll `<ws-id>` returned 401"
Bearer token mismatch. Two common causes:
- The token in `MOLECULE_WORKSPACE_TOKENS` doesn't match the workspace whose ID is in the corresponding position of `MOLECULE_WORKSPACE_IDS`. Verify same-order pairing.
- The workspace was rotated and the token was revoked. Generate a new token from the canvas Settings tab (or `POST /admin/workspaces/:id/tokens`).
### "molecule channel: poll `<ws-id>` returned 404"
Either the workspace doesn't exist or the `MOLECULE_PLATFORM_URL` is wrong. Confirm:
```bash
curl -fsS "$MOLECULE_PLATFORM_URL/workspaces/$WS_ID" \
-H "Authorization: Bearer $WS_TOKEN" | jq '.workspace.id'
```
### A2A messages aren't surfacing
Check that the watched workspace is actually receiving them — the plugin only pulls `activity_logs` rows whose `activity_type = a2a_receive`. If peers aren't sending to this workspace, there's nothing to surface. Verify with:
```bash
curl -fsS "$MOLECULE_PLATFORM_URL/workspaces/$WS_ID/activity?type=a2a_receive&limit=10" \
-H "Authorization: Bearer $WS_TOKEN" | jq
```
If that returns events but Claude doesn't see them, file an issue at [`molecule-mcp-claude-channel`](https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel/issues) with the workspace_id + sample event.
---
## Limitations (v0.1)
- **Polling-only inbound.** No push mode yet; latency floor is `MOLECULE_POLL_INTERVAL_MS`.
- **No pairing flow.** Tokens are configured manually via `.env`; no canvas-side approval handshake.
- **No file-attachment download.** URLs surface in the meta block; the host fetches on-demand.
- **No outbound channel-init.** The plugin only sends replies (in response to inbound A2A); starting a fresh A2A conversation initiated FROM the Claude Code side requires a future `start_workspace_chat` tool.
Track the v0.2 roadmap on the [plugin repo's README](https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel#limitations-v01).
---
## See also
- [External Agent Registration](/docs/guides/external-agent-registration) — full A2A wire-shape reference + Python SDK + curl flow
- [External Workspace Quickstart](/docs/guides/external-workspace-quickstart) — 5-min guide for any HTTP-speaking agent
- [Remote Workspaces FAQ](/docs/guides/remote-workspaces-faq) — production hardening notes
- [`molecule-mcp-claude-channel`](https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel) — plugin source code, issues, v0.2 roadmap
-214
View File
@@ -1,214 +0,0 @@
---
title: "a2a-sdk v0 → v1 migration"
description: "Cheat sheet for migrating workspace runtime code (and forks) from a2a-sdk 0.3.x to 1.x — renamed/removed symbols, common error shapes, before/after diffs."
---
import { Callout } from 'fumadocs-ui/components/callout';
The `a2a-sdk` Python package released v1.0 in late April 2026. The
Molecule workspace runtime migrated under tracking ID **KI-009** and
shipped in `molecule-ai-workspace-runtime` **v0.1.11** (commit
`d5cf872`, PR #39). The platform now runs exclusively on v1.
If you're consuming the platform's published wheel, bumping
`molecule-ai-workspace-runtime>=0.1.11` handles the migration for
you. If you maintain a fork of the runtime, an external agent talking
A2A directly, or your own adapter that imports from `a2a.*`, this page
is your checklist.
## Why migrate
- **Upstream**: `a2a-sdk` 1.0 reorganised the import surface, flattened
`Part`, removed deprecated capability flags, and replaced the
`A2AStarletteApplication` wrapper with explicit Starlette route
factories.
- **Platform**: as of 2026-04-24 the platform sends/receives via v1
shapes natively. The SDK ships a v0_3 compat layer (enabled in the
runtime via `enable_v0_3_compat=True` on `create_jsonrpc_routes`) so
in-flight 0.x callers don't break, but new code should target v1.
- **Forks/external runtimes**: v0 code throws on `import a2a.utils`
and `from a2a.server.apps import A2AStarletteApplication` once you
install v1, so the migration is a hard cutover at install time, not
a soft deprecation.
## Cheat sheet — renamed and removed symbols
The four breaking changes that hit the Molecule runtime during KI-009.
All four are confirmed against
`molecule-core/workspace/` source.
### 1. `new_agent_text_message` renamed to `new_text_message`
- **v0 location**: `a2a.utils.new_agent_text_message`
- **v1 location**: `a2a.helpers.new_text_message`
Both the module path and the symbol name changed.
### 2. `Part` API flattened — `TextPart` removed
- **v0**: `Part(root=TextPart(text="..."))` — `Part` wrapped a `root`
union of `TextPart` / `FilePart` / `DataPart`.
- **v1**: `Part(text="...")` — `Part` accepts the text payload
directly. `TextPart` no longer exists as a public symbol.
`FilePart` / `DataPart` are similarly flattened (`Part(file=...)`,
`Part(data=...)`); the Molecule runtime only emits text parts so the
file/data shapes weren't exercised in KI-009 and aren't covered by
this guide.
### 3. `A2AStarletteApplication` removed — use route factories
- **v0**: `from a2a.server.apps import A2AStarletteApplication` then
`A2AStarletteApplication(agent_card, request_handler).build()`.
- **v1**: `from a2a.server.routes import create_agent_card_routes,
create_jsonrpc_routes` then build a Starlette app from the returned
route lists.
The factories also let you mount the JSON-RPC endpoint at any path
(the runtime mounts at `/` because the platform POSTs to root, see
`workspace/main.py:279`).
### 4. `state_transition_history` capability flag removed
- **v0**: `AgentCapabilities(streaming=..., push_notifications=...,
state_transition_history=True)` was a per-agent opt-in.
- **v1**: the field is gone from `AgentCapabilities`. Per the SDK's own
`a2a/compat/v0_3/conversions.py`: *"No longer supported in v1.0"*.
The capability is now universal — `Task.history` is always available
and `tasks/get` accepts `historyLength` via `apply_history_length()`.
If you pass `state_transition_history=...` as a kwarg to
`AgentCapabilities` under v1, Pydantic will reject it. Drop the kwarg.
See [`workspace/main.py:215`](https://git.moleculesai.app/Molecule-AI/molecule-core/blob/main/workspace/main.py#L215)
for the explanatory comment that prevents future accidental re-adds.
## Common error shapes
When v0 code runs against the v1 SDK, the failure modes look like this:
| Error | Cause |
|---|---|
| `ModuleNotFoundError: No module named 'a2a.utils'` | v0 import path; module renamed to `a2a.helpers`. |
| `ImportError: cannot import name 'A2AStarletteApplication' from 'a2a.server.apps'` | The whole `a2a.server.apps` module is gone in v1. Switch to `a2a.server.routes` factories. |
| `ImportError: cannot import name 'TextPart' from 'a2a.types'` | Flattened `Part` API; use `Part(text=...)`. |
| `ValueError: Protocol message AgentCapabilities has no "state_transition_history" field` | Removed capability flag passed as kwarg; drop it. |
| `ValueError: Protocol message Part has no "root" field` | v0 `Part(root=TextPart(...))` shape against v1 schema; flatten to `Part(text=...)`. |
The protobuf-style `ValueError` messages always follow the pattern
`Protocol message <Type> has no "<field>" field` — that's the
fingerprint of "v0 shape against v1 schema." Treat it as a v0→v1 hint
even if the field name isn't on the cheat sheet above.
## Migration checklist
1. **Bump the dep** — `a2a-sdk[http-server]>=0.3.25` is the floor; remove
any `<1.0` upper bound. The Molecule wheel uses
`a2a-sdk[http-server]>=0.3.25` with no upper bound (see
[`molecule-ai-workspace-runtime/pyproject.toml`](https://git.moleculesai.app/Molecule-AI/molecule-ai-workspace-runtime/blob/main/pyproject.toml)).
2. **Fix imports** — sweep the four renamed/removed symbols above. A
safe grep is `grep -rn "from a2a\\|import a2a"` across your tree.
3. **Fix removed-field reads/writes** — search for
`state_transition_history` usage and delete the kwarg/field access.
4. **Flatten `Part` constructors** — search for `Part(root=` and
convert to `Part(text=...)` / `Part(file=...)` / `Part(data=...)`.
5. **Replace the app factory** — search for `A2AStarletteApplication`
and rewrite the bootstrap using `create_agent_card_routes` +
`create_jsonrpc_routes`. Pass `enable_v0_3_compat=True` to
`create_jsonrpc_routes` if your peers may still be on v0.
6. **Re-run tests** — fixture-level mocks of `a2a.helpers` /
`a2a.utils` need to mock both names so tests still pass during the
rename rollout (see
[`workspace/tests/conftest.py:105-111`](https://git.moleculesai.app/Molecule-AI/molecule-core/blob/main/workspace/tests/conftest.py#L105-L111)
for the dual-name pattern).
## Before / after diffs
### `new_agent_text_message` → `new_text_message`
```diff
-from a2a.utils import new_agent_text_message
+from a2a.helpers import new_text_message
async def execute(self, context, event_queue):
- await event_queue.enqueue_event(new_agent_text_message("hello"))
+ await event_queue.enqueue_event(new_text_message("hello"))
```
### Flat `Part` API
```diff
-from a2a.types import Part, TextPart
+from a2a.types import Part
-msg_parts = [Part(root=TextPart(text=final_text))]
+msg_parts = [Part(text=final_text)]
```
### `AgentCapabilities` — drop `state_transition_history`
```diff
capabilities=AgentCapabilities(
streaming=config.a2a.streaming,
push_notifications=config.a2a.push_notifications,
- state_transition_history=True,
),
```
### `A2AStarletteApplication` → route factories
```diff
-from a2a.server.apps import A2AStarletteApplication
+from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
-app = A2AStarletteApplication(
- agent_card=agent_card,
- http_handler=request_handler,
-).build()
+routes = []
+routes.extend(create_agent_card_routes(agent_card))
+routes.extend(create_jsonrpc_routes(
+ request_handler=request_handler,
+ rpc_url="/",
+ enable_v0_3_compat=True,
+))
+app = Starlette(routes=routes)
```
The `enable_v0_3_compat=True` flag on `create_jsonrpc_routes` is what
keeps in-flight v0 callers (peers that haven't migrated yet) from
breaking — it accepts the old method names and translates them. The
Molecule runtime ships with this flag on (see
[`workspace/main.py:279`](https://git.moleculesai.app/Molecule-AI/molecule-core/blob/main/workspace/main.py#L279));
strip it once your entire fleet is on v1.
## For downstream consumers
- **Using the published wheel** (`pip install
molecule-ai-workspace-runtime>=0.1.11`): the migration is in the
wheel — no code changes needed in your adapter or workspace template
beyond bumping the pin.
- **Running a fork of the runtime**: cherry-pick or rebase against
commit `d5cf872` ("feat: migrate a2a-sdk 1.x (KI-009) (#39)") in
`molecule-ai-workspace-runtime`. The diff is the canonical reference
for what KI-009 actually changed.
- **Standalone external agent** (talking A2A without the wheel): apply
the [Migration checklist](#migration-checklist) directly to your
source. The four cheat-sheet items are the entire surface that
changed for the typical agent role; only `Part` flattening and the
`state_transition_history` removal affect on-the-wire shapes — the
other two are import-only.
<Callout type="info">
The wheel keeps `enable_v0_3_compat=True` on `create_jsonrpc_routes`,
so a v0 peer can still hit a v1 wheel and vice versa during the
migration window. You don't need to coordinate a fleet-wide cutover —
migrate at your own pace.
</Callout>
## See also
- [`molecule-ai-workspace-runtime` v0.1.11 release](https://git.moleculesai.app/Molecule-AI/molecule-ai-workspace-runtime/releases/tag/v0.1.11) — first wheel containing KI-009
- [PR #39 — feat: migrate a2a-sdk 1.x (KI-009)](https://git.moleculesai.app/Molecule-AI/molecule-ai-workspace-runtime/pulls/39)
- [PR #48 — feat(a2a): dual-compat for a2a-sdk 0.3.x and 1.x](https://git.moleculesai.app/Molecule-AI/molecule-ai-workspace-runtime/pulls/48) — runtime-side compat shim that keeps v0 peers working against the v1 wheel
- [Bring Your Own Runtime (MCP)](/docs/runtime-mcp) — universal wheel install path
- [External Agents](/docs/external-agents) — manual A2A path for non-MCP runtimes