Merge pull request #2304 from Molecule-AI/docs/molecule-channel-plugin-pointer

docs: surface molecule-mcp-claude-channel plugin in external-workspace flow + CONTRIBUTING
This commit is contained in:
Hongming Wang 2026-04-30 00:45:51 +00:00 committed by GitHub
commit 0b1d4f294b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 5 deletions

View File

@ -175,6 +175,17 @@ and run CI manually.
- Type hints on public functions
- pytest for all tests
## External integrations
Code in this repo lands in molecule-core. Some related runtime artifacts
live in their own repos:
- [`Molecule-AI/molecule-ai-workspace-runtime`](https://github.com/Molecule-AI/molecule-ai-workspace-runtime) — Python adapter SDK (`molecule_runtime`) that runs inside containerized Molecule workspaces. Bridges Claude Code SDK / hermes / langgraph / etc. → A2A queue.
- [`Molecule-AI/molecule-sdk-python`](https://github.com/Molecule-AI/molecule-sdk-python) — `A2AServer` + `RemoteAgentClient` for external agents that register over the public `/registry/register` flow.
- [`Molecule-AI/molecule-mcp-claude-channel`](https://github.com/Molecule-AI/molecule-mcp-claude-channel) — Claude Code channel plugin. Bridges A2A traffic into a running Claude Code session via MCP `notifications/claude/channel`. Polling-based (no tunnel required); install with `claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel`.
When extending the **A2A surface** in molecule-core (`workspace-server/internal/handlers/a2a_proxy.go` etc.), consider whether the change has a downstream impact on the runtime SDK or the channel plugin — they're versioned independently but share the wire shape.
## Architecture Overview
See `CLAUDE.md` for detailed architecture documentation, including:

View File

@ -1,3 +1,5 @@
'use client';
// ExternalConnectModal — shown once after creating a runtime="external"
// workspace. Surfaces the workspace_auth_token + ready-to-paste snippets
// so the operator can hand them to whoever runs their off-host agent
@ -24,6 +26,12 @@ export interface ExternalConnectionInfo {
heartbeat_endpoint: string;
curl_register_template: string;
python_snippet: string;
// Claude Code channel plugin snippet — for operators whose external
// agent IS a Claude Code session. Polling-based; no tunnel required.
// Optional in the type for backward compat with platforms that
// haven't shipped molecule-core PR #2304 yet (older response payload
// omits the field; tab is hidden if empty).
claude_code_channel_snippet?: string;
}
interface Props {
@ -31,10 +39,14 @@ interface Props {
onClose: () => void;
}
type Tab = "python" | "curl" | "fields";
type Tab = "python" | "curl" | "claude" | "fields";
export function ExternalConnectModal({ info, onClose }: Props) {
const [tab, setTab] = useState<Tab>("python");
// Default to Claude Code when the platform offers it — that's the
// newest + simplest path (no tunnel needed). Falls back to Python
// for older platform builds that don't ship the snippet.
const initialTab: Tab = info?.claude_code_channel_snippet ? "claude" : "python";
const [tab, setTab] = useState<Tab>(initialTab);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const copy = useCallback(async (value: string, key: string) => {
@ -70,6 +82,13 @@ export function ExternalConnectModal({ info, onClose }: Props) {
'WORKSPACE_AUTH_TOKEN="<paste from create response>"',
`WORKSPACE_AUTH_TOKEN="${info.auth_token}"`,
);
// The channel snippet asks the operator to paste the auth_token into
// the .env file's MOLECULE_WORKSPACE_TOKENS field. Stamp it server-side
// here so the copy-paste-block is truly ready-to-run.
const filledChannel = info.claude_code_channel_snippet?.replace(
'MOLECULE_WORKSPACE_TOKENS=<paste auth_token from create response>',
`MOLECULE_WORKSPACE_TOKENS=${info.auth_token}`,
);
return (
<Dialog.Root open onOpenChange={(o) => !o && onClose()}>
@ -91,7 +110,11 @@ export function ExternalConnectModal({ info, onClose }: Props) {
aria-label="Connection snippet format"
className="mt-4 flex gap-1 border-b border-zinc-800"
>
{(["python", "curl", "fields"] as Tab[]).map((t) => (
{(
filledChannel
? (["claude", "python", "curl", "fields"] as Tab[])
: (["python", "curl", "fields"] as Tab[])
).map((t) => (
<button
key={t}
type="button"
@ -104,17 +127,32 @@ export function ExternalConnectModal({ info, onClose }: Props) {
: "border-transparent text-zinc-500 hover:text-zinc-300"
}`}
>
{t === "python" ? "Python SDK" : t === "curl" ? "curl" : "Fields"}
{t === "claude"
? "Claude Code"
: t === "python"
? "Python SDK"
: t === "curl"
? "curl"
: "Fields"}
</button>
))}
</div>
{/* Snippet area */}
<div className="mt-3">
{tab === "claude" && filledChannel && (
<SnippetBlock
value={filledChannel}
label="Claude Code channel — polls workspace's A2A; no tunnel needed"
copyKey="claude"
copied={copiedKey === "claude"}
onCopy={() => copy(filledChannel, "claude")}
/>
)}
{tab === "python" && (
<SnippetBlock
value={filledPython}
label="Python (recommended — includes heartbeat loop)"
label="Python SDK — includes heartbeat loop (push-mode, needs public URL)"
copyKey="python"
copied={copiedKey === "python"}
onCopy={() => copy(filledPython, "python")}

View File

@ -67,6 +67,37 @@ curl -fsS -X POST "{{PLATFORM_URL}}/registry/register" \
}'
`
// externalChannelTemplate — Claude Code channel plugin install + .env. For
// operators whose external agent IS a Claude Code session (laptop or
// remote dev VM); routes the workspace's A2A traffic into the running
// Claude Code session as conversation turns via MCP. The plugin source
// lives at github.com/Molecule-AI/molecule-mcp-claude-channel — polling
// based, no tunnel required (uses /workspaces/:id/activity?since_secs=,
// platform-side support shipped in #2300).
const externalChannelTemplate = `# Claude Code channel bridges this workspace's A2A traffic into your
# Claude Code session. No tunnel/public URL needed (polling-based).
#
# 1. Save this token + workspace_id, then create ~/.claude/channels/molecule/.env:
mkdir -p ~/.claude/channels/molecule
cat > ~/.claude/channels/molecule/.env <<'EOF'
MOLECULE_PLATFORM_URL={{PLATFORM_URL}}
MOLECULE_WORKSPACE_IDS={{WORKSPACE_ID}}
MOLECULE_WORKSPACE_TOKENS=<paste auth_token from create response>
EOF
chmod 600 ~/.claude/channels/molecule/.env
# 2. Launch Claude Code with the channel enabled:
claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel
# Inbound A2A messages now surface as conversation turns. Claude's
# replies route back via the reply_to_workspace MCP tool no extra
# wiring on your side.
#
# Multi-workspace: comma-separate IDs and tokens (same order). See
# https://github.com/Molecule-AI/molecule-mcp-claude-channel for
# pairing flow, push-mode upgrade, and v0.2 roadmap.
`
// externalPythonTemplate uses molecule-sdk-python's RemoteAgentClient +
// A2AServer (PR #13 in that repo). Until the SDK cuts a v0.y release
// to PyPI the snippet pins git+main.

View File

@ -370,6 +370,17 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
"{{PLATFORM_URL}}", platformURL),
"{{WORKSPACE_ID}}", id,
),
// Claude Code channel plugin snippet. For operators
// whose external agent IS a Claude Code session —
// the snippet sets up ~/.claude/channels/molecule/.env
// and points at the canonical first-party plugin at
// github.com/Molecule-AI/molecule-mcp-claude-channel.
// Polling-based; no tunnel needed.
"claude_code_channel_snippet": strings.ReplaceAll(
strings.ReplaceAll(externalChannelTemplate,
"{{PLATFORM_URL}}", platformURL),
"{{WORKSPACE_ID}}", id,
),
}
}
c.JSON(http.StatusCreated, resp)