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:
commit
0b1d4f294b
@ -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:
|
||||
|
||||
@ -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")}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user