diff --git a/canvas/src/components/ExternalConnectModal.tsx b/canvas/src/components/ExternalConnectModal.tsx index a10fb54c..3c1c3af1 100644 --- a/canvas/src/components/ExternalConnectModal.tsx +++ b/canvas/src/components/ExternalConnectModal.tsx @@ -32,6 +32,14 @@ export interface ExternalConnectionInfo { // haven't shipped molecule-core PR #2304 yet (older response payload // omits the field; tab is hidden if empty). claude_code_channel_snippet?: string; + // Universal MCP snippet — runtime-agnostic outbound tool path via + // the `molecule-mcp` console script in the + // molecule-ai-workspace-runtime PyPI wheel. Works with any MCP-aware + // agent runtime (Claude Code, hermes, codex, third-party). Outbound- + // only: pair with claude_code_channel or python tabs for heartbeat + // + inbound. Optional for backward compat with platforms that + // haven't shipped PR #2413 yet. + universal_mcp_snippet?: string; } interface Props { @@ -39,7 +47,7 @@ interface Props { onClose: () => void; } -type Tab = "python" | "curl" | "claude" | "fields"; +type Tab = "python" | "curl" | "claude" | "mcp" | "fields"; export function ExternalConnectModal({ info, onClose }: Props) { // Default to Claude Code when the platform offers it — that's the @@ -89,6 +97,17 @@ export function ExternalConnectModal({ info, onClose }: Props) { 'MOLECULE_WORKSPACE_TOKENS=', `MOLECULE_WORKSPACE_TOKENS=${info.auth_token}`, ); + // Universal MCP snippet uses MOLECULE_WORKSPACE_TOKEN as the env-var + // name passed through to molecule-mcp via `claude mcp add ... -- env + // MOLECULE_WORKSPACE_TOKEN=...`. The placeholder must match the + // template's literal — pre-2026-04-30 polish this looked for + // WORKSPACE_AUTH_TOKEN (carryover from the curl tab), which silently + // skipped the substitution and left "" + // visible in the operator's clipboard. + const filledUniversalMcp = info.universal_mcp_snippet?.replace( + 'MOLECULE_WORKSPACE_TOKEN=""', + `MOLECULE_WORKSPACE_TOKEN="${info.auth_token}"`, + ); return ( !o && onClose()}> @@ -110,11 +129,19 @@ export function ExternalConnectModal({ info, onClose }: Props) { aria-label="Connection snippet format" className="mt-4 flex gap-1 border-b border-zinc-800" > - {( - filledChannel - ? (["claude", "python", "curl", "fields"] as Tab[]) - : (["python", "curl", "fields"] as Tab[]) - ).map((t) => ( + {(() => { + // Build the tab order dynamically. Claude Code first + // (when offered) since it's the simplest setup; Python + // SDK second (full register+heartbeat+inbound); Universal + // MCP third (any MCP-aware runtime, outbound-only); curl + // for one-shot register; Fields for raw values. + const tabs: Tab[] = []; + if (filledChannel) tabs.push("claude"); + tabs.push("python"); + if (filledUniversalMcp) tabs.push("mcp"); + tabs.push("curl", "fields"); + return tabs; + })().map((t) => (