import { useEffect, useState } from "react"; import { trackEvent } from "../api/analytics"; import CursorIcon from "@lobehub/icons/es/Cursor/components/Mono"; import ClaudeIcon from "@lobehub/icons/es/Claude/components/Color"; import OpenCodeIcon from "@lobehub/icons/es/OpenCode/components/Mono"; import { ChevronDown } from "lucide-react"; import { CodeBlock } from "./code-block"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs"; import { CardStack, CardStackHeader, CardStackContent } from "./card-stack"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible"; import { NativeSelect, NativeSelectOption } from "./native-select"; import { Switch } from "./switch"; import { cn } from "../lib/utils"; import { useOrganizationSlug } from "../api/organization-context"; import { getExecutorServerAuthorizationHeader, useExecutorServerConnection, } from "../api/server-connection"; type TransportMode = "stdio" | "http"; export type McpElicitationMode = "browser" | "model" | "native"; const SUPPORTED_AGENTS = [ { key: "cursor", label: "Cursor", Icon: CursorIcon }, { key: "claude", label: "Claude", Icon: ClaudeIcon }, { key: "opencode", label: "OpenCode", Icon: OpenCodeIcon }, ] as const; const isDev = import.meta.env.DEV; const devCliCwd = import.meta.env.VITE_EXECUTOR_DEV_CLI_CWD as string | undefined; const currentLocation = globalThis.window?.location; const isLocal = currentLocation?.hostname === "localhost" || currentLocation?.hostname === "127.0.0.1" || currentLocation?.hostname.endsWith(".localhost") === true; export const shellQuoteWord = (value: string): string => { if (/^[A-Za-z0-9_/:=@%+.,-]+$/.test(value)) return value; return `'${value.replace(/'/g, `'"'"'`)}'`; }; const hasDesktopConnectionBridge = (): boolean => { return Boolean(globalThis.window?.executor?.getServerConnection); }; export const buildMcpHttpEndpoint = (input: { readonly origin: string | null; readonly desktop?: { readonly port: number; } | null; readonly elicitationMode?: McpElicitationMode; /** Artifacts are on by default, so only the opt-out is spelled out on the * URL (`&artifacts=false`) and a default endpoint stays clean. */ readonly artifacts?: boolean; // Cloud only: pins the URL to `//mcp` (the server also accepts the // legacy `//mcp` form). Desktop/local pass nothing and get the bare // `/mcp` path. readonly organizationSlug?: string | null; }): string => { // The desktop sidecar isn't org-scoped, so the org only applies to the // origin/remote forms. const mcpPath = input.organizationSlug && !input.desktop ? `/${input.organizationSlug}/mcp` : "/mcp"; const endpoint = input.desktop ? `http://127.0.0.1:${input.desktop.port}${mcpPath}` : input.origin ? `${input.origin}${mcpPath}` : `${mcpPath}`; // Only non-default choices reach the URL, so the common endpoint has no query // at all: `model` elicitation and artifacts-on are what the server assumes. const params: Array = []; if (input.elicitationMode && input.elicitationMode !== "model") { params.push(["elicitation_mode", input.elicitationMode]); } if (input.artifacts === false) params.push(["artifacts", "false"]); if (params.length === 0) return endpoint; const query = params.map(([key, value]) => `${key}=${value}`).join("&"); // The `` placeholder is not a parsable URL — concatenate. if (endpoint.startsWith("<")) return `${endpoint}?${query}`; const url = new URL(endpoint); for (const [key, value] of params) url.searchParams.set(key, value); return url.toString(); }; export const buildMcpInstallCommand = (input: { readonly mode: TransportMode; readonly isDev: boolean; readonly origin: string | null; readonly scopeDir?: string; readonly desktop?: { readonly port: number; } | null; readonly authorizationHeader?: string | null; readonly elicitationMode?: McpElicitationMode; readonly artifacts?: boolean; readonly devCliCwd?: string; readonly organizationSlug?: string | null; }): string => { if (input.mode === "http") { const endpoint = buildMcpHttpEndpoint({ origin: input.origin, desktop: input.desktop ? { port: input.desktop.port } : null, elicitationMode: input.elicitationMode, artifacts: input.artifacts, organizationSlug: input.organizationSlug, }); const headerFlags: string[] = []; if (input.authorizationHeader) { headerFlags.push(`--header ${shellQuoteWord(`Authorization: ${input.authorizationHeader}`)}`); } const parts = [ `npx add-mcp ${shellQuoteWord(endpoint)} --transport http --name executor`, ...headerFlags, ]; return parts.join(" "); } const innerArgs = input.isDev ? input.devCliCwd ? ["bun", "run", "--cwd", input.devCliCwd, "dev:cli", "mcp"] : ["bun", "run", "dev:cli", "mcp"] : ["executor", "mcp"]; if (input.scopeDir) { innerArgs.push("--scope", input.scopeDir); } if (input.elicitationMode && input.elicitationMode !== "model") { innerArgs.push("--elicitation-mode", input.elicitationMode); } if (input.artifacts === false) { innerArgs.push("--no-artifacts"); } return `npx add-mcp ${shellQuoteWord(innerArgs.map(shellQuoteWord).join(" "))} --name executor`; }; export function McpInstallCard(props: { className?: string }) { const [mode, setMode] = useState("http"); const [advancedOpen, setAdvancedOpen] = useState(false); const [httpElicitationMode, setHttpElicitationMode] = useState("model"); const [artifacts, setArtifacts] = useState(true); const organizationSlug = useOrganizationSlug(); const serverConnection = useExecutorServerConnection(); // Desktop hosts ship Electron without putting an `executor` binary on // PATH, and the bundled sidecar is locked to the running app. Force the // HTTP path there; it routes through the active sidecar connection. const showStdio = isLocal && serverConnection.kind !== "desktop-sidecar" && !hasDesktopConnectionBridge(); const elicitationMode = mode === "stdio" ? "model" : httpElicitationMode; // The desktop renderer's connection carries no auth (the main process injects // the bearer at the session layer). For the install command — which an // EXTERNAL agent runs and therefore needs the token in plaintext — fetch it // on demand from the bridge. const [desktopAuthToken, setDesktopAuthToken] = useState(null); useEffect(() => { if (serverConnection.kind !== "desktop-sidecar") { setDesktopAuthToken(null); return; } let cancelled = false; void globalThis.window?.executor?.getServerAuthToken?.().then( (token) => { if (!cancelled) setDesktopAuthToken(token); }, () => undefined, ); return () => { cancelled = true; }; }, [serverConnection.kind]); const authorizationHeader = getExecutorServerAuthorizationHeader(serverConnection) ?? (desktopAuthToken ? `Bearer ${desktopAuthToken}` : null); const command = buildMcpInstallCommand({ mode, isDev, origin: serverConnection.origin, authorizationHeader, elicitationMode, artifacts, devCliCwd, organizationSlug, }); const subtitle = mode === "stdio" ? isDev ? "Uses the repo-local dev CLI from any agent working directory." : "Requires the executor CLI on your PATH." : "Paste this into Claude Code, Cursor, or any MCP client, and your agent gets every tool you connect here."; const advancedControls = ( Advanced
Artifacts
{artifacts ? "Generated UI components are saved to your workspace." : "Disabled: this connection serves no artifact tools."}
{ setArtifacts(next); trackEvent("mcp_install_artifacts_toggled", { artifacts: next }); }} aria-label="Artifacts" />
Resume approvals
{mode === "http" ? "Select how tool approvals are handled for this Remote HTTP connection." : "Standard I/O exposes a resume tool to the model. Use Remote HTTP for browser approvals."}
{ const next = event.target.value as McpElicitationMode; setHttpElicitationMode(next); trackEvent("mcp_install_elicitation_mode_changed", { elicitation_mode: next }); }} aria-label="Elicitation mode" className="min-w-44" > {mode === "http" && ( Browser approval )} Model resume tool {mode === "http" && ( Native elicitation )}
); const agentLogos = (
Work with your agent
{SUPPORTED_AGENTS.map(({ key, label, Icon }, index) => ( 0 && "-ml-2 group-hover/agents:ml-1", )} > ))}
and more
); const header = ( Remote HTTP Standard I/O ) : undefined } >
Connect an agent {subtitle}
); const body = (
trackEvent("mcp_install_command_copied", { transport: mode, elicitation_mode: elicitationMode, surface: "integrations", }) } /> {advancedControls &&
{advancedControls}
}
{agentLogos}
); return ( {showStdio ? ( { const next = v as TransportMode; setMode(next); trackEvent("mcp_install_transport_switched", { transport: next }); }} > {header} {body} {body} ) : ( <> {header} {body} )} ); }