> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bfl.ml/llms.txt
> Use this file to discover all available pages before exploring further.

# FLUX MCP server

> Generate images and video, edit, vary, browse, and reuse FLUX results from any MCP-compatible client. OAuth sign-in, no API keys to manage.

export const MutedVideo = ({src, poster, alt = "", aspectRatio = "16 / 9", borderRadius = "0.75rem"}) => {
  const videoRef = useRef(null);
  const [muted, setMuted] = useState(true);
  const start = event => {
    const v = event.currentTarget;
    v.muted = muted;
    const p = v.play?.();
    if (p && typeof p.catch === "function") p.catch(() => {});
  };
  const toggleMute = () => {
    const v = videoRef.current;
    const next = !muted;
    setMuted(next);
    if (v) v.muted = next;
  };
  const iconBtn = {
    position: "absolute",
    top: "0.9rem",
    right: "0.9rem",
    zIndex: 3,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    width: "2.4rem",
    height: "2.4rem",
    padding: 0,
    borderRadius: "999px",
    border: "none",
    background: "rgba(12, 14, 18, 0.62)",
    backdropFilter: "blur(6px)",
    color: "#fff",
    cursor: "pointer",
    transition: "background 160ms ease"
  };
  return <div className="not-prose" style={{
    position: "relative",
    width: "100%",
    aspectRatio,
    borderRadius,
    overflow: "hidden",
    background: "#0c0e12"
  }}>
      <video ref={videoRef} src={src} poster={poster} autoPlay loop muted={muted} playsInline preload="metadata" onCanPlay={start} onLoadedData={start} aria-label={alt} style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    objectFit: "cover",
    display: "block"
  }} />
      <button type="button" onClick={toggleMute} aria-label={muted ? "Unmute" : "Mute"} style={iconBtn}>
        {muted ? <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
            <line x1="23" y1="9" x2="17" y2="15" />
            <line x1="17" y1="9" x2="23" y2="15" />
          </svg> : <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
            <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
            <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
          </svg>}
      </button>
    </div>;
};

export const ImageComparisonSlider = ({beforeImage, afterImage, beforeLabel = "Before", afterLabel = "After", height = "500px", objectFit = "cover", garmentImage, garmentLabel = "Garment reference"}) => {
  const [position, setPosition] = useState(50);
  const dialogRef = useRef(null);
  const openLightbox = () => {
    if (dialogRef.current && typeof dialogRef.current.showModal === "function") {
      dialogRef.current.showModal();
    }
  };
  const closeLightbox = () => {
    if (dialogRef.current && typeof dialogRef.current.close === "function") {
      dialogRef.current.close();
    }
  };
  const getPosition = (e, container) => {
    const rect = container.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const x = clientX - rect.left;
    return Math.max(0, Math.min(100, x / rect.width * 100));
  };
  const onPointerDown = e => {
    e.preventDefault();
    e.stopPropagation();
    const container = e.currentTarget;
    setPosition(getPosition(e, container));
    const onMove = ev => {
      ev.preventDefault();
      setPosition(getPosition(ev, container));
    };
    const onUp = () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      window.removeEventListener("touchmove", onMove);
      window.removeEventListener("touchend", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    window.addEventListener("touchmove", onMove, {
      passive: false
    });
    window.addEventListener("touchend", onUp);
  };
  return <div className="not-prose" style={{
    borderRadius: "1rem",
    overflow: "hidden",
    height,
    width: "100%"
  }}>
      <div onMouseDown={onPointerDown} onTouchStart={onPointerDown} onClick={e => {
    e.preventDefault();
    e.stopPropagation();
  }} style={{
    position: "relative",
    width: "100%",
    height,
    overflow: "hidden",
    cursor: "ew-resize",
    userSelect: "none",
    WebkitUserSelect: "none"
  }}>
        {}
        <img src={afterImage} alt={afterLabel} draggable={false} style={{
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    objectFit,
    pointerEvents: "none"
  }} />

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: 0,
    width: "100%",
    height: "100%",
    clipPath: `inset(0 ${100 - position}% 0 0)`
  }}>
          <img src={beforeImage} alt={beforeLabel} draggable={false} style={{
    display: "block",
    width: "100%",
    height: "100%",
    objectFit,
    pointerEvents: "none"
  }} />
        </div>

        {}
        <div style={{
    position: "absolute",
    top: 0,
    left: `${position}%`,
    transform: "translateX(-50%)",
    width: "3px",
    height: "100%",
    background: "rgba(255,255,255,0.85)",
    pointerEvents: "none",
    zIndex: 2
  }} />

        {}
        <div style={{
    position: "absolute",
    top: "50%",
    left: `${position}%`,
    transform: "translate(-50%, -50%)",
    width: "44px",
    height: "44px",
    borderRadius: "50%",
    background: "rgba(255,255,255,0.95)",
    border: "2px solid rgba(0,0,0,0.15)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    gap: "6px",
    zIndex: 3,
    pointerEvents: "none",
    boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
  }}>
          {}
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginRight: "-2px"
  }}>
            <path d="M8 1L2 7L8 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          {}
          <svg width="10" height="14" viewBox="0 0 10 14" fill="none" style={{
    marginLeft: "-2px"
  }}>
            <path d="M2 1L8 7L2 13" stroke="#333" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </div>

        {}
        <div style={{
    position: "absolute",
    top: "12px",
    left: "12px",
    padding: "4px 10px",
    borderRadius: "6px",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    fontSize: "0.75rem",
    fontWeight: 600,
    letterSpacing: "0.02em",
    pointerEvents: "none",
    zIndex: 4
  }}>
          {beforeLabel}
        </div>
        <div style={{
    position: "absolute",
    top: "12px",
    right: "12px",
    padding: "4px 10px",
    borderRadius: "6px",
    background: "rgba(0,0,0,0.55)",
    backdropFilter: "blur(4px)",
    color: "#fff",
    fontSize: "0.75rem",
    fontWeight: 600,
    letterSpacing: "0.02em",
    pointerEvents: "none",
    zIndex: 4
  }}>
          {afterLabel}
        </div>

        {}
        {garmentImage && <div onMouseDown={e => e.stopPropagation()} onTouchStart={e => e.stopPropagation()} onClick={e => {
    e.preventDefault();
    e.stopPropagation();
    openLightbox();
  }} title={`${garmentLabel} — click to enlarge`} style={{
    position: "absolute",
    bottom: "12px",
    right: "12px",
    height: "140px",
    maxWidth: "40%",
    borderRadius: "8px",
    overflow: "hidden",
    cursor: "zoom-in",
    background: "rgba(255,255,255,0.95)",
    border: "2px solid rgba(255,255,255,0.9)",
    boxShadow: "0 2px 12px rgba(0,0,0,0.45)",
    zIndex: 5,
    display: "flex",
    alignItems: "center",
    justifyContent: "center"
  }}>
            <img src={garmentImage} alt={garmentLabel} draggable={false} style={{
    height: "100%",
    width: "auto",
    objectFit: "contain",
    pointerEvents: "none"
  }} />
          </div>}
      </div>

      {}
      {garmentImage && <dialog ref={dialogRef} onClick={closeLightbox} onClose={closeLightbox} style={{
    padding: 0,
    border: "none",
    background: "transparent",
    maxWidth: "100vw",
    maxHeight: "100vh",
    width: "100vw",
    height: "100vh",
    overflow: "hidden"
  }}>
          <div onClick={closeLightbox} style={{
    width: "100vw",
    height: "100vh",
    background: "rgba(0,0,0,0.88)",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    cursor: "zoom-out",
    padding: "40px",
    boxSizing: "border-box"
  }}>
            <img src={garmentImage} alt={garmentLabel} onClick={e => e.stopPropagation()} style={{
    maxWidth: "90vw",
    maxHeight: "90vh",
    objectFit: "contain",
    borderRadius: "8px",
    boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
    cursor: "default"
  }} />
          </div>
        </dialog>}
    </div>;
};

export const FluxImageGrid = ({images, cols = 2, gap = 8, radius = "0.5rem", objectFit = "cover"}) => {
  return <div style={{
    display: "grid",
    gridTemplateColumns: `repeat(${cols}, 1fr)`,
    gap: `${gap}px`,
    width: "100%",
    height: "100%",
    minHeight: 0
  }}>
      {images.map((src, idx) => <div key={idx} style={{
    position: "relative",
    width: "100%",
    height: "100%",
    borderRadius: radius,
    overflow: "hidden",
    background: "#0f0f12"
  }}>
          <img src={src} alt="" draggable={false} style={{
    position: "absolute",
    inset: 0,
    width: "100%",
    height: "100%",
    objectFit,
    display: "block"
  }} />
        </div>)}
    </div>;
};

export const FluxChatCard = ({prompt, status, children, title}) => {
  return <div style={{
    width: "100%",
    height: "100%",
    background: "#1a1a1d",
    borderRadius: "1.125rem",
    padding: "1.5rem 1.75rem",
    display: "flex",
    flexDirection: "column",
    gap: "1rem",
    fontFamily: "ui-sans-serif, -apple-system, system-ui, sans-serif",
    color: "#e8e8ea",
    boxSizing: "border-box",
    overflow: "hidden",
    border: "1px solid rgba(255,255,255,0.06)"
  }}>
      {}
      <div style={{
    display: "flex",
    justifyContent: "flex-end"
  }}>
        <div style={{
    background: "#2a2a2e",
    color: "#fff",
    padding: "0.5rem 0.95rem",
    borderRadius: "1.125rem",
    fontSize: "0.92rem",
    fontWeight: 400,
    maxWidth: "78%",
    lineHeight: 1.4,
    letterSpacing: "-0.005em"
  }}>
          {prompt}
        </div>
      </div>

      {}
      <div style={{
    display: "flex",
    alignItems: "center"
  }}>
        <img src="https://bfl.ai/brand/logotype-white.svg" alt="Black Forest Labs" draggable={false} style={{
    height: "22px",
    width: "auto",
    display: "block"
  }} />
      </div>

      {}
      {status && <div style={{
    fontSize: "0.86rem",
    color: "#a8a8ad",
    lineHeight: 1.45,
    marginTop: "-0.25rem"
  }}>
          {status}
        </div>}

      {}
      <div style={{
    flex: 1,
    minHeight: 0,
    display: "flex",
    flexDirection: "column"
  }}>
        {children}
      </div>
    </div>;
};

export const MCPShowcase = ({slides, children, height = "560px", maxWidth = "960px", marginTop = "0", interval = 4500, fadeMs = 700}) => {
  const [activeIdx, setActiveIdx] = useState(0);
  const [paused, setPaused] = useState(false);
  const [timerId, setTimerId] = useState(null);
  const childArray = !children ? [] : Array.isArray(children) ? children.filter(Boolean) : [children];
  const useChildren = childArray.length > 0;
  const items = useChildren ? childArray : slides || [];
  if (!paused && !timerId && items.length > 1) {
    const id = setInterval(() => {
      setActiveIdx(p => (p + 1) % items.length);
    }, interval);
    setTimerId(id);
  }
  const pause = () => {
    if (timerId) clearInterval(timerId);
    setTimerId(null);
    setPaused(true);
  };
  const resume = () => setPaused(false);
  const goTo = idx => {
    pause();
    setActiveIdx((idx + items.length) % items.length);
  };
  const prev = () => goTo(activeIdx - 1);
  const next = () => goTo(activeIdx + 1);
  const captionFor = item => {
    if (useChildren) return item?.props?.title || "";
    return item?.title || "";
  };
  const arrowBase = {
    width: "26px",
    height: "26px",
    borderRadius: "999px",
    border: "1px solid rgba(255,255,255,0.18)",
    background: "rgba(255,255,255,0.08)",
    color: "rgba(255,255,255,0.9)",
    cursor: "pointer",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    padding: 0,
    transition: "background 200ms ease, border-color 200ms ease"
  };
  return <div className="not-prose" onMouseEnter={pause} onMouseLeave={resume} style={{
    position: "relative",
    width: "100%",
    maxWidth,
    margin: `${marginTop} auto 0`,
    borderRadius: "1.25rem",
    overflow: "hidden",
    background: "radial-gradient(ellipse at top, #1e1f24 0%, #0a0a0c 75%)",
    border: "1px solid rgba(255,255,255,0.08)",
    boxShadow: "0 30px 80px -30px rgba(0,0,0,0.5)"
  }}>
      <div style={{
    position: "relative",
    width: "100%",
    height
  }}>
        {items.map((item, idx) => <div key={idx} style={{
    position: "absolute",
    inset: 0,
    display: "flex",
    alignItems: "stretch",
    justifyContent: "stretch",
    padding: useChildren ? "1.75rem 1.75rem 4.5rem" : "2.5rem 2.5rem 5rem",
    opacity: idx === activeIdx ? 1 : 0,
    transform: idx === activeIdx ? "scale(1)" : "scale(0.985)",
    transition: `opacity ${fadeMs}ms ease, transform ${fadeMs}ms ease`,
    pointerEvents: idx === activeIdx ? "auto" : "none"
  }}>
            {useChildren ? <div style={{
    width: "100%",
    height: "100%",
    display: "flex"
  }}>{item}</div> : <img src={item.img} alt={item.title} draggable={false} style={{
    maxWidth: "100%",
    maxHeight: "100%",
    margin: "auto",
    objectFit: "contain",
    borderRadius: "0.625rem",
    boxShadow: "0 25px 50px -12px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.05)"
  }} />}
          </div>)}

        <div style={{
    position: "absolute",
    bottom: 0,
    left: 0,
    right: 0,
    padding: "0.875rem 1.75rem 1.125rem",
    display: "flex",
    justifyContent: "space-between",
    alignItems: "flex-end",
    gap: "1rem",
    zIndex: 3,
    background: "linear-gradient(to top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0) 100%)",
    pointerEvents: "none"
  }}>
          <div style={{
    color: "#fff",
    fontSize: "0.92rem",
    fontWeight: 500,
    letterSpacing: "-0.01em",
    textShadow: "0 1px 4px rgba(0,0,0,0.4)",
    maxWidth: "70%"
  }}>
            {captionFor(items[activeIdx])}
          </div>
          <div style={{
    display: "flex",
    gap: "10px",
    alignItems: "center",
    pointerEvents: "auto"
  }}>
            {items.length > 1 && <button type="button" onClick={prev} aria-label="Previous slide" style={arrowBase} onMouseEnter={e => {
    e.currentTarget.style.background = "rgba(255,255,255,0.16)";
    e.currentTarget.style.borderColor = "rgba(255,255,255,0.3)";
  }} onMouseLeave={e => {
    e.currentTarget.style.background = "rgba(255,255,255,0.08)";
    e.currentTarget.style.borderColor = "rgba(255,255,255,0.18)";
  }}>
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <polyline points="15 18 9 12 15 6" />
                </svg>
              </button>}
            <div style={{
    display: "flex",
    gap: "8px",
    alignItems: "center"
  }}>
              {items.map((_, idx) => <button key={idx} onClick={() => goTo(idx)} aria-label={`Go to slide ${idx + 1}`} style={{
    width: activeIdx === idx ? "28px" : "8px",
    height: "8px",
    borderRadius: "4px",
    border: "none",
    padding: 0,
    cursor: "pointer",
    background: activeIdx === idx ? "rgba(255,255,255,0.95)" : "rgba(255,255,255,0.3)",
    transition: "all 350ms ease"
  }} />)}
            </div>
            {items.length > 1 && <button type="button" onClick={next} aria-label="Next slide" style={arrowBase} onMouseEnter={e => {
    e.currentTarget.style.background = "rgba(255,255,255,0.16)";
    e.currentTarget.style.borderColor = "rgba(255,255,255,0.3)";
  }} onMouseLeave={e => {
    e.currentTarget.style.background = "rgba(255,255,255,0.08)";
    e.currentTarget.style.borderColor = "rgba(255,255,255,0.18)";
  }}>
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <polyline points="9 18 15 12 9 6" />
                </svg>
              </button>}
          </div>
        </div>
      </div>
    </div>;
};

export const mcpSetupTabs = prompt => [{
  id: 'claude',
  label: 'Claude',
  logo: 'claude',
  steps: [{
    number: 1,
    title: 'Open your connectors',
    description: 'This opens <strong>claude.ai</strong> on <strong>Customize → Connectors</strong>. In Claude Desktop, the same list is under Settings → Connectors.',
    button: {
      label: 'Open connectors',
      href: 'https://claude.ai/customize/connectors',
      logo: 'claude'
    }
  }, {
    number: 2,
    title: 'Add a custom connector',
    description: 'Choose to add a custom connector, then paste in these two values.',
    codes: [{
      label: 'Name',
      value: 'FLUX'
    }, {
      label: 'URL',
      value: 'https://mcp.bfl.ai'
    }]
  }, {
    number: 3,
    title: 'Sign in',
    description: 'Click <strong>Connect</strong>, sign in with your BFL account, and choose the organization you want billed.'
  }]
}, {
  id: 'claude-code',
  label: 'Claude Code',
  logo: 'claude',
  steps: [{
    number: 1,
    title: 'Let Claude Code set itself up',
    description: 'Opens a Claude Code session with the setup prompt filled in. Press Enter and it registers the server and verifies the connection. Works through <strong>Claude Desktop</strong>, or through the CLI once <code>claude-cli://</code> is registered.',
    button: {
      label: 'Open in Claude Code',
      href: `claude://code/new?q=${encodeURIComponent(prompt)}`,
      logo: 'claude'
    }
  }, {
    number: 2,
    title: 'Or add it yourself',
    description: 'One command in your terminal registers FLUX as a remote MCP server.',
    code: 'claude mcp add --transport http FLUX https://mcp.bfl.ai',
    wide: true
  }, {
    number: 3,
    title: 'Sign in',
    description: 'A browser opens on first use. Sign in, choose the organization you want billed, then return to Claude Code.'
  }]
}, {
  id: 'cursor',
  label: 'Cursor',
  logo: 'cursor',
  steps: [{
    number: 1,
    title: 'Install with one click',
    description: 'Click the button to open Cursor with the FLUX server pre-filled. Cursor will ask you to approve the install.',
    button: {
      label: 'Add to Cursor',
      href: 'cursor://anysphere.cursor-deeplink/mcp/install?name=FLUX&config=eyJ1cmwiOiJodHRwczovL21jcC5iZmwuYWkifQ==',
      logo: 'cursor'
    }
  }, {
    number: 2,
    title: 'Approve the install',
    description: 'In Cursor, confirm <strong>Add server</strong> in the approval dialog. The entry is saved to your global <code>~/.cursor/mcp.json</code>.'
  }, {
    number: 3,
    title: 'Sign in',
    description: 'The first FLUX request opens a browser for OAuth sign-in. Tokens refresh automatically after that.'
  }],
  configCaption: 'Prefer manual setup? Paste this into .cursor/mcp.json instead:',
  configCode: `{
  "mcpServers": {
    "FLUX": {
      "url": "https://mcp.bfl.ai"
    }
  }
}`
}, {
  id: 'codex',
  label: 'Codex',
  logo: 'codex',
  steps: [{
    number: 1,
    title: 'Let Codex set itself up',
    description: 'Opens the Codex app with the setup prompt filled in. Send it and the agent registers the server and verifies the connection.',
    button: {
      label: 'Open in Codex',
      href: `codex://threads/new?prompt=${encodeURIComponent(prompt)}`,
      logo: 'codex'
    }
  }, {
    number: 2,
    title: 'Or add it yourself',
    description: 'One command in your terminal registers FLUX as a remote MCP server.',
    code: 'codex mcp add FLUX --url https://mcp.bfl.ai',
    wide: true
  }, {
    number: 3,
    title: 'Sign in',
    description: 'Codex detects OAuth support and opens a browser automatically. Sign in, choose the organization you want billed, then start a new Codex session so the FLUX tools are loaded.'
  }]
}, {
  id: 'vscode',
  label: 'VS Code',
  logo: 'vscode',
  steps: [{
    number: 1,
    title: 'Install with one click',
    description: 'Click the button to open VS Code with the FLUX server pre-filled. VS Code will ask you to confirm the install.',
    button: {
      label: 'Add to VS Code',
      href: `vscode:mcp/install?${encodeURIComponent(JSON.stringify({
        name: 'FLUX',
        type: 'http',
        url: 'https://mcp.bfl.ai'
      }))}`,
      logo: 'vscode'
    }
  }, {
    number: 2,
    title: 'Start the server',
    description: 'Open the Chat view, pick <strong>Agent</strong> mode, and make sure FLUX is enabled in the tools picker. VS Code starts the server on demand.'
  }, {
    number: 3,
    title: 'Sign in',
    description: 'VS Code prompts you to authenticate the FLUX server. Sign in with your BFL account and choose the organization you want billed.'
  }],
  configCaption: 'Prefer manual setup? Paste this into .vscode/mcp.json (or your user mcp.json) instead:',
  configCode: `{
  "servers": {
    "FLUX": {
      "type": "http",
      "url": "https://mcp.bfl.ai"
    }
  }
}`
}, {
  id: 'devin',
  label: 'Devin',
  logo: 'devin',
  steps: [{
    number: 1,
    title: 'Open the MCP settings',
    description: 'In Devin, go to <strong>Settings → Connections → MCP servers</strong> and click <strong>Add a custom MCP</strong>. That button needs the <strong>Manage MCP Servers</strong> permission. Without it, use <strong>Suggest MCP Integration</strong> to ask an org admin.',
    button: {
      label: 'Devin MCP docs',
      href: 'https://docs.devin.ai/work-with-devin/mcp',
      logo: 'devin'
    }
  }, {
    number: 2,
    title: 'Add the FLUX server',
    description: 'Name it <code>FLUX</code>, add a short description, select the <strong>HTTP</strong> transport type, and enter <code>https://mcp.bfl.ai</code> as the server URL. Set the authentication method to <strong>OAuth</strong>.'
  }, {
    number: 3,
    title: 'Choose access, then test',
    description: 'Under <strong>Access</strong>, pick <strong>Organization</strong> to share one connection (connect a service account, not a personal one) or <strong>Personal</strong> so each member authenticates individually. Click <strong>Save</strong>, then <strong>Test listing tools</strong>. Devin prompts you to complete the OAuth flow during your first session, where you sign in and choose the organization you want billed.'
  }],
  configCaption: 'Prefer the Devin CLI? Run devin mcp add FLUX https://mcp.bfl.ai to write the entry below to .devin/mcp_config.local.json, then devin mcp login FLUX to authenticate:',
  configCode: `{
  "mcpServers": {
    "FLUX": {
      "url": "https://mcp.bfl.ai",
      "transport": "http"
    }
  }
}`
}, {
  id: 'mcp-remote',
  label: 'Other clients',
  logo: 'terminal',
  steps: [{
    number: 1,
    title: 'Run the bridge',
    description: 'Use this when your client supports stdio MCP servers but does not handle the OAuth flow on its own — for example <strong>Hermes</strong> (Nous Research) and similar tools that only accept static bearer tokens. A browser opens for sign-in; tokens cache to <code>~/.mcp-auth/</code> and refresh automatically.',
    code: 'npx -y mcp-remote https://mcp.bfl.ai',
    wide: true
  }, {
    number: 2,
    title: 'Add it to your config',
    description: 'Register FLUX as a local stdio server in your client’s MCP config. Use the JSON example below and adapt the server name if needed.',
    button: {
      label: 'mcp-remote docs',
      href: 'https://www.npmjs.com/package/mcp-remote'
    }
  }, {
    number: 3,
    title: 'Restart the client',
    description: 'Your client talks to local stdio. mcp-remote forwards requests to <code>https://mcp.bfl.ai</code> over HTTP and keeps OAuth tokens fresh.'
  }],
  configCaption: 'For stdio-based clients, the MCP config usually looks like this:',
  configCode: `{
  "mcpServers": {
    "FLUX": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.bfl.ai"]
    }
  }
}`
}];

export const agentPrompt = `Connect the FLUX MCP server to this agent, then verify it works.

Use https://docs.bfl.ai/llms.txt as the source of truth for FLUX setup, models, and API reference — prefer it over inventing steps.

Steps:
1. If FLUX MCP tools are already available in this agent, skip installation and go to step 4.
2. Register the remote HTTP MCP server https://mcp.bfl.ai under the name "FLUX":
   - Claude Code: run \`claude mcp add --transport http FLUX https://mcp.bfl.ai\`
   - Codex: run \`codex mcp add FLUX --url https://mcp.bfl.ai\`, then \`codex mcp login FLUX\`
   - Cursor: add {"mcpServers":{"FLUX":{"url":"https://mcp.bfl.ai"}}} to ~/.cursor/mcp.json
   - Devin CLI: run \`devin mcp add FLUX https://mcp.bfl.ai\`, then \`devin mcp login FLUX\`
   - VS Code: add {"servers":{"FLUX":{"type":"http","url":"https://mcp.bfl.ai"}}} to .vscode/mcp.json
   - Clients without remote MCP or OAuth support: register a stdio server that runs \`npx -y mcp-remote https://mcp.bfl.ai\`
3. Authentication is OAuth with a BFL account — a browser window opens on first use. Let me complete the sign-in and pick the billed organization myself. Do not complete OAuth through an embedded or automated browser, and never ask me for an API key.
4. Verify the connection by calling the FLUX get_credits tool and report my remaining credit balance.
5. Briefly tell me what I can do now: generate and edit images, generate video, create variations, and browse my generation history, all by prompting.`;

export const CLIENT_LOGOS = {
  claude: {
    viewBox: '0 0 24 24',
    fill: '#D97757',
    d: 'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z'
  },
  codex: {
    viewBox: '0 0 320 320',
    fill: 'currentColor',
    d: 'm297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z'
  },
  cursor: {
    viewBox: '0 0 466.73 532.09',
    fill: 'currentColor',
    d: 'M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z'
  },
  devin: {
    viewBox: '0 0 192 192',
    fill: 'currentColor',
    d: 'M 46.5 30.4 L 32.5 38.6 32.2 56.2 L 32 73.8 34.7 75.7 C 40.3 79.6, 60.7 91, 62 91 C 62.7 91, 66.9 88.9, 71.2 86.4 L 79 81.8 82.9 81.3 L 86.8 80.8 90.4 82 L 94 83.2 96.8 86.8 L 99.5 90.3 99.5 96.6 L 99.5 102.9 96 106.5 L 92.6 110 88.7 111.1 L 84.7 112.1 82.1 111.5 C 80.7 111.2, 76 108.9, 71.8 106.5 C 67.5 104, 63.1 102, 62 102 C 60.9 102, 53.7 105.6, 46 110.1 L 32 118.2 32 136 L 32 153.7 46.1 161.9 C 53.9 166.3, 61 170, 62 170 C 63 170, 70.1 166.4, 77.8 162 L 91.8 154 92.2 142.7 L 92.5 131.5 94.4 128.5 C 95.4 126.8, 97.6 124.4, 99.4 123.2 L 102.6 121 107.6 121 L 112.6 121 121.3 126 C 126.1 128.8, 130.4 131, 130.8 131 C 131.2 131, 138 127.2, 146 122.6 L 160.5 114.2 160.5 96.4 L 160.5 78.5 146.5 70.3 C 138.8 65.8, 131.7 62.1, 130.7 62 C 129.8 62, 127.3 63.1, 125.2 64.4 C 123.2 65.7, 119 68, 116.1 69.5 L 110.6 72.2 106.2 71.7 L 101.8 71.2 99.1 69.3 C 97.7 68.2, 95.6 65.8, 94.5 63.9 L 92.5 60.5 92 49.5 L 91.5 38.5 77.5 30.3 C 69.8 25.8, 62.8 22.2, 62 22.2 C 61.2 22.2, 54.2 25.9, 46.5 30.4'
  },
  vscode: {
    viewBox: '0 0 24 24',
    fill: '#007ACC',
    d: 'M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z'
  }
};

export const SetupDivider = ({label = 'or'}) => <div className="not-prose flex items-center gap-4 my-10">
    <span className="flex-1 h-px bg-[var(--aspen-surface-strong)] dark:bg-white/10" />
    <span className="text-sm font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500">{label}</span>
    <span className="flex-1 h-px bg-[var(--aspen-surface-strong)] dark:bg-white/10" />
  </div>;

export const AgentSetup = ({prompt, serverName = 'FLUX', serverUrl = 'https://mcp.bfl.ai', caption, step, title, subtitle}) => {
  const [copied, setCopied] = useState(false);
  const [missing, setMissing] = useState(null);
  const renderLogo = (id, size, className) => {
    if (!id) return null;
    if (id === 'terminal') {
      return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
          <polyline points="4 17 10 11 4 5" />
          <line x1="12" y1="19" x2="20" y2="19" />
        </svg>;
    }
    const logo = CLIENT_LOGOS[id];
    if (!logo) return null;
    return <svg width={size} height={size} viewBox={logo.viewBox} xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden="true">
        <path fill={logo.fill} d={logo.d} />
      </svg>;
  };
  const enc = encodeURIComponent(prompt);
  const cursorConfig = typeof btoa !== 'undefined' ? btoa(JSON.stringify({
    url: serverUrl
  })) : '';
  const vscodeConfig = encodeURIComponent(JSON.stringify({
    name: serverName,
    type: 'http',
    url: serverUrl
  }));
  const actions = [{
    id: 'claude-code',
    logo: 'claude',
    label: 'Open in Claude Code',
    hint: 'Opens with the setup prompt ready to send',
    hrefs: [`claude://code/new?q=${enc}`, `claude-cli://open?q=${enc}`],
    app: 'Claude Code',
    install: 'https://claude.com/product/claude-code'
  }, {
    id: 'codex',
    logo: 'codex',
    label: 'Open in Codex',
    hint: 'Opens with the setup prompt ready to send',
    href: `codex://threads/new?prompt=${enc}`,
    app: 'Codex',
    install: 'https://developers.openai.com/codex'
  }, {
    id: 'cursor',
    logo: 'cursor',
    label: 'Add to Cursor',
    hint: 'Installs the server directly',
    href: `cursor://anysphere.cursor-deeplink/mcp/install?name=${serverName}&config=${cursorConfig}`,
    app: 'Cursor',
    install: 'https://cursor.com'
  }, {
    id: 'vscode',
    logo: 'vscode',
    label: 'Add to VS Code',
    hint: 'Installs the server directly',
    href: `vscode:mcp/install?${vscodeConfig}`,
    app: 'VS Code',
    install: 'https://code.visualstudio.com'
  }, {
    id: 'claude',
    logo: 'claude',
    label: 'Add to Claude',
    hint: 'Opens your Claude connectors',
    href: 'https://claude.ai/customize/connectors',
    external: true
  }];
  const copyPrompt = () => {
    if (typeof navigator !== 'undefined' && navigator.clipboard) {
      navigator.clipboard.writeText(prompt);
      setCopied(true);
      setTimeout(() => setCopied(false), 1500);
    }
  };
  const launch = (event, action) => {
    if (!action.app || typeof window === 'undefined') return;
    event.preventDefault();
    setMissing(null);
    let left = false;
    const markLeft = () => {
      left = true;
    };
    const onVisibility = () => {
      if (document.hidden) left = true;
    };
    window.addEventListener('blur', markLeft);
    window.addEventListener('pagehide', markLeft);
    document.addEventListener('visibilitychange', onVisibility);
    const cleanup = () => {
      window.removeEventListener('blur', markLeft);
      window.removeEventListener('pagehide', markLeft);
      document.removeEventListener('visibilitychange', onVisibility);
    };
    const targets = action.hrefs || [action.href];
    targets.forEach((href, i) => {
      setTimeout(() => {
        if (left) return;
        window.location.href = href;
      }, i * 600);
    });
    setTimeout(() => {
      cleanup();
      if (!left) setMissing(action.id);
    }, 1400 + (targets.length - 1) * 600);
  };
  return <div className="not-prose my-8">
      {title && <div className="mb-4">
          <div className="flex items-center gap-3">
            {step && <span className="flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-full bg-primary dark:bg-primary-light text-white dark:text-gray-900 font-semibold text-sm">
                {step}
              </span>}
            <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 m-0">{title}</h3>
          </div>
          {subtitle && <p className="text-sm text-gray-600 dark:text-gray-400 mt-2 mb-0">{subtitle}</p>}
        </div>}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
        {actions.map(a => <a key={a.id} href={a.href || (a.hrefs || [])[0]} target={a.external ? '_blank' : undefined} rel={a.external ? 'noopener noreferrer' : undefined} title={a.hint} onClick={e => launch(e, a)} className="group flex items-center justify-center gap-3 w-full rounded-xl border border-[var(--aspen-surface-strong)] dark:border-white/15 bg-[var(--aspen-paper)] dark:bg-white/10 hover:bg-[var(--aspen-surface)] dark:hover:bg-white/20 hover:border-[var(--aspen-stone)] dark:hover:border-white/30 active:translate-y-px shadow-sm hover:shadow px-6 py-5 text-base font-semibold text-gray-900 dark:text-white no-underline transition-all">
            {renderLogo(a.logo, 22, 'flex-shrink-0')}
            {a.label}
          </a>)}
        <button onClick={copyPrompt} className="group flex items-center justify-center gap-3 w-full rounded-xl border border-[var(--aspen-surface-strong)] dark:border-white/15 bg-[var(--aspen-paper)] dark:bg-white/10 hover:bg-[var(--aspen-surface)] dark:hover:bg-white/20 hover:border-[var(--aspen-stone)] dark:hover:border-white/30 active:translate-y-px shadow-sm hover:shadow px-6 py-5 text-base font-semibold text-gray-900 dark:text-white no-underline transition-all">
          {copied ? <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0">
              <path d="M20 6 9 17l-5-5" />
            </svg> : <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0">
              <rect x="9" y="9" width="13" height="13" rx="2" />
              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
            </svg>}
          {copied ? 'Copied' : 'Copy setup prompt'}
        </button>
      </div>
      {missing && <div className="flex items-start gap-2.5 mt-4 rounded-lg border border-amber-200 dark:border-amber-500/25 bg-amber-50 dark:bg-amber-500/10 px-4 py-3">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="flex-shrink-0 mt-0.5 text-amber-600 dark:text-amber-400">
            <circle cx="12" cy="12" r="10" />
            <path d="M12 8v4" />
            <path d="M12 16h.01" />
          </svg>
          <p className="text-sm text-amber-900 dark:text-amber-200 m-0">
            {`You don't seem to have ${(actions.find(a => a.id === missing) || ({})).app} installed. `}
            <a href={(actions.find(a => a.id === missing) || ({})).install} target="_blank" rel="noopener noreferrer" className="underline font-medium text-amber-900 dark:text-amber-200">
              Install it
            </a>
            {' or try another client above.'}
          </p>
        </div>}
      {caption && <p className="text-xs text-gray-500 dark:text-gray-400 mt-4 mb-0">{caption}</p>}
    </div>;
};

export const SetupSteps = ({tabs, queryParam = 'client', step, title, subtitle}) => {
  const [activeId, setActiveId] = useState(tabs[0].id);
  const renderLogo = (id, size, className) => {
    if (!id) return null;
    if (id === 'terminal') {
      return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
          <polyline points="4 17 10 11 4 5" />
          <line x1="12" y1="19" x2="20" y2="19" />
        </svg>;
    }
    const logo = CLIENT_LOGOS[id];
    if (!logo) return null;
    return <svg width={size} height={size} viewBox={logo.viewBox} xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden="true">
        <path fill={logo.fill} d={logo.d} />
      </svg>;
  };
  const [copiedKey, setCopiedKey] = useState(null);
  const active = tabs.find(t => t.id === activeId) ?? tabs[0];
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const params = new URLSearchParams(window.location.search);
    const fromUrl = params.get(queryParam);
    if (fromUrl && tabs.some(t => t.id === fromUrl)) {
      setActiveId(fromUrl);
    }
  }, [queryParam, tabs]);
  const selectTab = id => {
    setActiveId(id);
    if (typeof window !== 'undefined') {
      const params = new URLSearchParams(window.location.search);
      params.set(queryParam, id);
      const newUrl = `${window.location.pathname}?${params.toString()}${window.location.hash}`;
      window.history.replaceState(null, '', newUrl);
    }
  };
  const copy = (key, value) => {
    if (typeof navigator !== 'undefined' && navigator.clipboard) {
      navigator.clipboard.writeText(value);
      setCopiedKey(key);
      setTimeout(() => setCopiedKey(curr => curr === key ? null : curr), 1500);
    }
  };
  return <div className="not-prose">
      {title && <div className="mb-6">
          <div className="flex items-center gap-3">
            {step && <span className="flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-full bg-primary dark:bg-primary-light text-white dark:text-gray-900 font-semibold text-sm">
                {step}
              </span>}
            <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 m-0">{title}</h3>
          </div>
          {subtitle && <p className="text-sm text-gray-600 dark:text-gray-400 mt-2 mb-0">{subtitle}</p>}
        </div>}
      <div className="flex justify-center mb-8">
        <div className="inline-flex flex-wrap justify-center gap-1 p-1 rounded-full bg-[var(--aspen-surface-strong)] dark:bg-white/5 border border-[var(--aspen-surface-strong)] dark:border-white/10">
          {tabs.map(tab => {
    const selected = activeId === tab.id;
    return <button key={tab.id} onClick={() => selectTab(tab.id)} className={`inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-full transition-colors ${selected ? 'bg-[var(--aspen-paper)] text-gray-900 shadow-sm dark:bg-white dark:text-gray-900' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white'}`}>
                {tab.logo && renderLogo(tab.logo, 15, 'flex-shrink-0')}
                {tab.label}
              </button>;
  })}
        </div>
      </div>

      <div className={`grid grid-cols-1 ${active.steps.some(s => s.wide) ? 'md:grid-cols-4' : 'md:grid-cols-3'} gap-4`}>
        {active.steps.map((step, i) => {
    const key = `${active.id}-${i}`;
    return <div key={key} className={`rounded-2xl border border-[var(--aspen-surface-strong)] dark:border-white/10 bg-[var(--aspen-paper)] dark:bg-white/5 p-6 flex flex-col ${step.wide ? 'md:col-span-2' : ''}`}>
              <div className="text-base font-semibold text-gray-400 dark:text-gray-500 mb-3">
                {step.number}
              </div>
              <h3 className="text-base font-semibold text-gray-900 dark:text-gray-100 mt-0 mb-2">
                {step.title}
              </h3>
              <p className="text-sm text-gray-600 dark:text-gray-400 mb-4 flex-1" dangerouslySetInnerHTML={{
      __html: step.description
    }} />
              {step.button && step.button.copyText && <button onClick={() => copy(`${key}-btn`, step.button.copyText)} className="self-start mt-1 inline-flex items-center gap-2 rounded-lg bg-[var(--aspen-surface)] dark:bg-white/10 hover:bg-[var(--aspen-surface-strong)] dark:hover:bg-white/15 text-gray-900 dark:text-gray-100 text-sm font-medium px-3 py-2">
                  {step.button.logo && renderLogo(step.button.logo, 14)}
                  {copiedKey === `${key}-btn` ? 'Copied' : step.button.label}
                  {copiedKey !== `${key}-btn` && <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                      <rect x="9" y="9" width="13" height="13" rx="2" />
                      <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
                    </svg>}
                </button>}
              {step.button && !step.button.copyText && <a href={step.button.href} target={step.button.href.startsWith('http') ? '_blank' : undefined} rel={step.button.href.startsWith('http') ? 'noopener noreferrer' : undefined} className="self-start mt-1 inline-flex items-center gap-2 rounded-lg bg-[var(--aspen-surface)] dark:bg-white/10 hover:bg-[var(--aspen-surface-strong)] dark:hover:bg-white/15 text-gray-900 dark:text-gray-100 text-sm font-medium px-3 py-2 no-underline">
                  {step.button.logo && renderLogo(step.button.logo, 14)}
                  {step.button.label}
                  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M7 17 17 7" />
                    <path d="M7 7h10v10" />
                  </svg>
                </a>}
              {step.code && <div className="mt-1 flex items-start justify-between gap-2 rounded-lg bg-[var(--aspen-surface-strong)] dark:bg-black/40 border border-[var(--aspen-stone)] dark:border-white/10 px-3 py-2">
                  <code className="text-xs sm:text-sm font-mono text-gray-700 dark:text-gray-200 break-all whitespace-pre-wrap min-w-0 flex-1">{step.code}</code>
                  <button onClick={() => copy(key, step.code)} className="flex-shrink-0 text-gray-400 hover:text-gray-700 dark:hover:text-gray-100 mt-0.5" aria-label="Copy">
                    {copiedKey === key ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                        <path d="M20 6 9 17l-5-5" />
                      </svg> : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                        <rect x="9" y="9" width="13" height="13" rx="2" />
                        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
                      </svg>}
                  </button>
                </div>}
              {step.codes && step.codes.map((c, ci) => {
      const codeKey = `${key}-c${ci}`;
      return <div key={ci} className={`${ci === 0 ? 'mt-1' : 'mt-2'} flex items-start gap-2 rounded-lg bg-[var(--aspen-surface-strong)] dark:bg-black/40 border border-[var(--aspen-stone)] dark:border-white/10 px-3 py-2`}>
                    {c.label && <span className="text-xs font-medium text-gray-500 dark:text-gray-400 flex-shrink-0 mt-0.5">
                        {c.label}
                      </span>}
                    <code className="text-xs sm:text-sm font-mono text-gray-700 dark:text-gray-200 break-all whitespace-pre-wrap min-w-0 flex-1">{c.value}</code>
                    <button onClick={() => copy(codeKey, c.value)} className="flex-shrink-0 text-gray-400 hover:text-gray-700 dark:hover:text-gray-100" aria-label="Copy">
                      {copiedKey === codeKey ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                          <path d="M20 6 9 17l-5-5" />
                        </svg> : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                          <rect x="9" y="9" width="13" height="13" rx="2" />
                          <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
                        </svg>}
                    </button>
                  </div>;
    })}
            </div>;
  })}
      </div>

      {active.configCode && <div className="mt-8">
          {active.configCaption && <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
              {active.configCaption}
            </p>}
          <div className="relative">
            <pre className="rounded-xl bg-[var(--aspen-surface-strong)] dark:bg-black/40 border border-[var(--aspen-stone)] dark:border-white/10 p-4 text-xs sm:text-sm font-mono text-gray-800 dark:text-gray-200 overflow-x-auto m-0">
              <code>{active.configCode}</code>
            </pre>
            <button onClick={() => copy(`${active.id}-config`, active.configCode)} className="absolute top-3 right-3 text-gray-400 hover:text-gray-700 dark:hover:text-gray-100" aria-label="Copy config">
              {copiedKey === `${active.id}-config` ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M20 6 9 17l-5-5" />
                </svg> : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="9" y="9" width="13" height="13" rx="2" />
                  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
                </svg>}
            </button>
          </div>
        </div>}
    </div>;
};

<a id="setup-instructions" />

<AgentSetup step="1" title="Set up in one click" subtitle="Click your client. The editor buttons install mcp.bfl.ai directly. The agent buttons open a session with a setup prompt ready to send, which registers the server and verifies it worked." prompt={agentPrompt} />

<SetupDivider />

<SetupSteps step="2" title="Set up manually" subtitle="Pick your client for the full steps. For stdio-only or OAuth-incompatible clients (for example Hermes), use Other clients: it runs mcp-remote locally, handles the browser OAuth flow, refreshes tokens for you, and exposes FLUX as a normal stdio server." tabs={mcpSetupTabs(agentPrompt)} />

**Bring FLUX into the tools you already use.** Generate options in parallel, edit attached images through prompts, branch into variations from any result you like, and generate video from the same conversation, inside Claude, Cursor, Codex, Devin, and any MCP-compatible client. No API code, no keys pasted into the conversation.

<MCPShowcase height="580px" maxWidth="960px" marginTop="3rem">
  <FluxChatCard title="Generate a batch of options in one prompt." prompt="Generate 4 editorial portrait variants — varied lighting, palette, and mood." status="Generating 4 options at 1440×1792…">
    <FluxImageGrid
      cols={4}
      images={[
    "https://cdn.sanity.io/images/2gpum2i6/production/c3f2d4e1211258390694350b73564199b0ec1f9b-1440x1792.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/a4016c043d3afb049d51381b0695d52411030026-1440x1792.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/d3e95a7892d15aa647ea098bdf66256cc5d4de31-1440x1792.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/f6eaa9ed0645cdd9df0ceeaf5962477958c15be4-1440x1792.jpg",
  ]}
    />
  </FluxChatCard>

  <FluxChatCard title="Or just one — when a single hero shot is what you need." prompt="A top-down aerial photograph of shallow desert salt pools, intense direct sunlight." status="Generating 1 image at 1920×1440…">
    <FluxImageGrid
      cols={1}
      objectFit="cover"
      images={[
    "https://cdn.sanity.io/images/2gpum2i6/production/0dc6f45f41c1e8d2b6606285b4c785d6cfc586e0-1920x1440.jpg",
  ]}
    />
  </FluxChatCard>

  <FluxChatCard title="Edit any image directly in chat — drag the slider to compare." prompt="Add a camel on the sand." status="Editing — preserved scene composition.">
    <ImageComparisonSlider beforeImage="https://cdn.sanity.io/images/2gpum2i6/production/0dc6f45f41c1e8d2b6606285b4c785d6cfc586e0-1920x1440.jpg" afterImage="https://cdn.sanity.io/images/2gpum2i6/production/627eb91f1da5299c96c1d591b0bbc48368c4e2f2-1920x1440.jpg" beforeLabel="Original" afterLabel="Edited" height="100%" />
  </FluxChatCard>

  <FluxChatCard title="Browse and reuse your full generation history." prompt="Show me my recent FLUX generations." status="Your recent generations, tap any tile to edit, vary, or download.">
    <FluxImageGrid
      cols={3}
      images={[
    "https://cdn.sanity.io/images/2gpum2i6/production/627eb91f1da5299c96c1d591b0bbc48368c4e2f2-1920x1440.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/0dc6f45f41c1e8d2b6606285b4c785d6cfc586e0-1920x1440.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/f6eaa9ed0645cdd9df0ceeaf5962477958c15be4-1440x1792.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/d3e95a7892d15aa647ea098bdf66256cc5d4de31-1440x1792.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/a4016c043d3afb049d51381b0695d52411030026-1440x1792.jpg",
    "https://cdn.sanity.io/images/2gpum2i6/production/c3f2d4e1211258390694350b73564199b0ec1f9b-1440x1792.jpg",
  ]}
    />
  </FluxChatCard>

  <FluxChatCard title="Generate video in the same conversation." prompt="A drop of indigo ink hitting still water, macro, backlit, blooming in slow motion." status="Generating a 5s clip at 16:9 with audio…">
    <MutedVideo src="https://cdn.sanity.io/files/2gpum2i6/production/6a66e4fbf54ec65b69c8b945cb4d99232f161f16.mp4" poster="https://cdn.sanity.io/images/2gpum2i6/production/81562ee5ae8cbf2a7baad958e9db71edd475c199-1280x704.jpg" alt="A drop of indigo ink blooming in water, generated as video through the FLUX MCP server" />
  </FluxChatCard>
</MCPShowcase>

## Pricing

<a id="pricing" />

You pay BFL directly. The organization selected during OAuth sign-in is billed for generated images. No shared quotas, no middleman. To change organizations, disconnect the connector and reconnect it. Current rates are listed at [bfl.ai/pricing](https://bfl.ai/pricing).

## Tool reference

The MCP server exposes a small set of tools. Your client decides which to call based on your prompt — you do not need to invoke them by name. The reference is here for developers who want to know exactly what is available.

<AccordionGroup>
  <Accordion title="Show all tools">
    | Tool                  | Purpose                                                                                                                                                                                                           | Notes                                                                                                                                                                                                                                                                                                   |
    | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `generate_image`      | Generate one or up to 8 images in parallel. Covers text-to-image, edits, multi-reference composition, style transfer, inpainting-style edits, and outpainting through prompts.                                    | Each entry in `requests` carries its own prompt, model, dimensions, seed, and up to 8 `input_image` slots. Outpainting uses `width`/`height` larger than the source.                                                                                                                                    |
    | `vto`                 | Virtual try-on. Dress a person, pet, or mascot in a garment, hat, sunglasses, shoes, bag, or any other wearable taken from a reference image. Face, hair, and pose stay as they were; only the worn item changes. | Takes a `person` image and a `garment` image, each as a prior `request_id` or a public URL. One garment slot per call, so merge a multi-piece outfit into a single reference image first.                                                                                                               |
    | `generate_variations` | Produce N more images "in the same direction" as a previous generation, identified by `request_id`.                                                                                                               | Reuses the original prompt, model, dimensions, and any input image slots. Defaults to 4 variations, max 8.                                                                                                                                                                                              |
    | `get_history`         | List recent generations as a thumbnail grid with per-tile actions (Variations, Edit, copy, download).                                                                                                             | Keyset pagination on `created_at` via `cursor`; supports `before` / `after` date filters and a `status` filter.                                                                                                                                                                                         |
    | `get_credits`         | Return the calling user's remaining BFL credit balance.                                                                                                                                                           | Useful when a generation fails for billing reasons.                                                                                                                                                                                                                                                     |
    | `generate_video`      | Generate video with FLUX 3. Covers text-to-video, animating or morphing between supplied keyframe images, and continuing from an existing clip.                                                                   | Each entry in `requests` sets `mode` (`t2v`, `i2v`, `v2v`) and a prompt, plus optional `duration` (5–20 seconds, or `auto`), `aspect_ratio`, and `resolution` (`hd` or `fhd`). Up to 4 clips per call. `generate_audio` is on by default. Returns a `request_id` immediately; see the video note below. |
    | `enhance_video`       | Re-render a finished draft at full quality, keeping the same composition, seed, and prompt plan.                                                                                                                  | Takes the `request_id` of a ready draft and finishes at `fhd` by default. This is the commit step of the draft-then-enhance loop.                                                                                                                                                                       |

    Available models on `generate_image`: `flux2_pro_preview` (default), `flux2_max` (highest quality), `flux2_klein_9b_preview` (faster, up to 4 input images), `flux2_flex` (best for typography), `flux2_klein_4b`. The full catalog, per-model reference-image limits, and the FLUX Tools are also exposed as the `bfl://models` MCP resource.

    Video runs on `flux3_video`.

    Note that `vto` is its own tool, not a model. Ask for a try-on in plain language and your client routes there; passing `vto` as a `model` value to `generate_image` fails.
  </Accordion>
</AccordionGroup>

<Note>
  **Video returns a `request_id` first.** `generate_video` responds with `{"status": "pending", "request_id": "…"}` right away. Clips take minutes, and long ones can take considerably longer. Visual clients keep polling on their own. In a terminal client, ask again or have your agent call `get_result(request_id="…")` to pick the clip up.

  **Draft first, then enhance.** Setting `draft: true` renders a cheap `hd` pass so you can judge motion and timing before committing. When you like it, `enhance_video` re-renders that same draft at full quality. Drafts are `hd` only.
</Note>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Tools do not appear after connecting">
    * In Claude Desktop or Claude.ai, open **Settings → Connectors** and confirm the FLUX connector shows as **Connected**.
    * If the connection failed silently, remove the connector and add it again. Make sure pop-ups are not blocked so the OAuth window can open.
    * In Claude Code, run `claude mcp list` to confirm the server is registered.
    * In Codex, run `codex mcp list` to confirm the `FLUX` server is registered, then start a new Codex session.
  </Accordion>

  <Accordion title="Refreshing tools or reconnecting the server">
    If the FLUX tools are not responding, or you just installed or updated the connector, refreshing the connection can help.

    * **Claude.ai / Claude Desktop:** open **Settings → Connectors**, toggle the FLUX connector off and on, or click **Reconnect**. Restarting Claude Desktop is another way to pick up a fresh tool list.
    * **Claude Code:** run `/mcp` to view server status and reauthenticate. To rebuild the registration entirely, run `claude mcp remove FLUX` followed by `claude mcp add --transport http FLUX https://mcp.bfl.ai`.
    * **Codex:** run `codex mcp login FLUX` to reauthenticate. To rebuild the registration entirely, run `codex mcp remove FLUX` followed by `codex mcp add FLUX --url https://mcp.bfl.ai` — the OAuth browser flow runs automatically on add.
    * **`mcp-remote` clients:** clearing the cached OAuth tokens with `rm -rf ~/.mcp-auth` and restarting the client triggers a fresh browser sign-in.
    * To confirm the tools are live, ask your MCP client something simple like *"check my BFL credits"*.
  </Accordion>

  <Accordion title="Authentication or billing errors">
    * Make sure you have a BFL account at [bfl.ai](https://bfl.ai).
    * Disconnect and reconnect the MCP server to redo the OAuth flow.
    * Check that the selected organization has sufficient credits.
    * Ask your MCP client to check your BFL credits if you want to verify the current balance.
  </Accordion>

  <Accordion title="A generation keeps loading">
    Large sets of images, FLUX.2 \[max], or complex edits can take longer than smaller generations. In Claude and other visual MCP clients, the image view keeps updating automatically.

    Video is slower by nature. You can expect a video to take minutes rather than seconds, on top of that duration and `fhd` resolution increase generation time. If your client has stopped showing progress, ask it to check the request again, or call `get_result` with the `request_id` from the original response. Use `draft: true` while you are still iterating.
  </Accordion>

  <Accordion title="Attached image editing fails">
    Your MCP client needs permission to upload attached images to BFL. If your client blocks outbound HTTPS from its sandbox, allow the `*.bfl.ai` domain or use a public image URL instead.
  </Accordion>

  <Accordion title="Image quality issues">
    * Use detailed prompts. Describe subject, style, composition, and lighting.
    * For typography or readable text, ask for FLUX.2 \[flex].
    * For hero shots or final assets, ask for FLUX.2 \[max].
    * For edits, say what should stay unchanged as well as what should change.
  </Accordion>

  <Accordion title="Switching the billed organization">
    Disconnect the FLUX connector in your client and reconnect it. The OAuth flow will prompt you to select an organization again.
  </Accordion>
</AccordionGroup>

## Prompt Tips

* **Front-load the subject.** Put the most important object, person, or scene first.
* **Describe lighting.** “Soft golden hour light” or “overcast diffused studio light” gives the model useful direction.
* **Use hex colors.** `#FF6B6B (coral pink)` is more precise than “pinkish red”.
* **Quote rendered text.** Use exact quoted strings for typography, labels, posters, and signs.
* **Avoid negative prompts.** FLUX responds to what you describe, not a list of what to avoid.
* **Iterate from results.** Use Variations for alternatives or Edit to keep refining a generated image.
* **For video, name one thing that happens.** A clip needs a subject, a camera behaviour, and a single motivated event. Describe the sound you want too, since audio is generated with the picture.

***

## Agent Skills

MCP and Agent Skills solve different problems:

|                  | MCP                                                                     | Agent Skills                                                                            |
| ---------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **What it does** | Generates, edits, varies, and browses images and video directly in chat | Teaches your coding agent how to write FLUX API code and how to direct FLUX generations |
| **Best for**     | Creative work inside Claude or another MCP client                       | Building applications that call the FLUX API                                            |
| **Install on**   | Claude Desktop, Claude.ai, Claude Code, and MCP-compatible clients      | Claude Code, Cursor, Windsurf, and other skill-compatible tools                         |

### Installation

<Tabs>
  <Tab title="Claude Code">
    ```bash theme={null}
    /plugin marketplace add black-forest-labs/skills
    /plugin install flux-image-best-practices@black-forest-labs
    /plugin install flux-3-video@black-forest-labs
    ```
  </Tab>

  <Tab title="Cursor">
    ```bash theme={null}
    npx skills add black-forest-labs/skills
    ```

    Or add manually by placing the skill files in `.cursor/skills/` in your project.
  </Tab>

  <Tab title="Other Tools">
    ```bash theme={null}
    npx skills add black-forest-labs/skills
    ```

    Skills follow the open [agentskills.io](https://agentskills.io) specification and work with any compatible tool.
  </Tab>
</Tabs>

### What Your Agent Learns

**flux-image-best-practices** teaches prompting patterns: prompt structure, lighting vocabulary, hex colors, typography, model selection, and why FLUX does not use negative prompts.

**bfl-api** teaches production API patterns: async generation, rate-limit handling, URL expiration, regional endpoints, webhook verification, and error handling.

**flux-3-video** is a set of six skills for video work. `flux-3-video` routes a request to the ones it needs; `flux-3-prompt-doctor` catches the decisions that change the payload before anything is generated; `flux-3-cinematic-inserts` covers shot craft for text-to-video; `flux-3-keyframes-continuation` covers building from supplied images or extending an existing clip; `flux-3-audio-dialogue` covers ambience, effects, and speech; and `flux-3-generate` covers submitting, polling, drafts, and downloads.

### Updating

```bash theme={null}
npx skills update black-forest-labs/skills
```

### Resources

* [BFL Skills on GitHub](https://github.com/black-forest-labs/skills)
* [agentskills.io specification](https://agentskills.io)
