> ## 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, edit, vary, browse, and reuse FLUX.2 images from any MCP-compatible client. OAuth sign-in, all FLUX.2 models, no API keys to manage.

export const ImageComparisonSlider = ({beforeImage, afterImage, beforeLabel = "Before", afterLabel = "After", height = "500px", objectFit = "cover"}) => {
  const [position, setPosition] = useState(50);
  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>
      </div>
    </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 SetupSteps = ({tabs, queryParam = 'client'}) => {
  const [activeId, setActiveId] = useState(tabs[0].id);
  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">
      <div className="flex justify-center mb-8">
        <div className="inline-flex gap-1 p-1 rounded-full bg-gray-100 dark:bg-white/5 border border-gray-200 dark:border-white/10">
          {tabs.map(tab => {
    const selected = activeId === tab.id;
    return <button key={tab.id} onClick={() => selectTab(tab.id)} className={`px-5 py-2 text-sm font-medium rounded-full transition-colors ${selected ? 'bg-white 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.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-gray-200 dark:border-white/10 bg-white dark:bg-[#0e1a14] p-6 flex flex-col ${step.wide ? 'md:col-span-2' : ''}`}>
              <div className="flex items-center justify-center w-9 h-9 rounded-full bg-primary dark:bg-primary-light text-white dark:text-gray-900 font-semibold text-sm mb-5">
                {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 && <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-gray-100 dark:bg-white/10 hover:bg-gray-200 dark:hover:bg-white/15 text-gray-900 dark:text-gray-100 text-sm font-medium px-3 py-2 no-underline">
                  {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-gray-100 dark:bg-black/40 border border-gray-200 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-gray-100 dark:bg-black/40 border border-gray-200 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-gray-100 dark:bg-black/40 border border-gray-200 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>;
};

export const setupTabs = [{
  id: 'claude',
  label: 'Claude',
  steps: [{
    number: 1,
    title: 'Open Claude settings',
    description: 'Launch Claude Desktop or open <strong>claude.ai</strong> and go to Settings → Connectors.',
    button: {
      label: 'Settings → Connectors',
      href: 'https://claude.ai/settings/connectors'
    }
  }, {
    number: 2,
    title: 'Add a custom connector',
    description: 'Click <strong>Add custom connector</strong>, then fill in the name and the server URL.',
    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',
  steps: [{
    number: 1,
    title: 'Open your terminal',
    description: 'Claude Code installed? You are ready.',
    button: {
      label: 'Install Claude Code',
      href: 'https://claude.com/product/claude-code'
    }
  }, {
    number: 2,
    title: 'Add the MCP server',
    description: 'This registers FLUX as a remote MCP server in your Claude Code config.',
    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',
  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=='
    }
  }, {
    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',
  steps: [{
    number: 1,
    title: 'Open your terminal',
    description: 'Codex installed? You are ready.',
    button: {
      label: 'Codex config docs',
      href: 'https://developers.openai.com/codex/config-reference'
    }
  }, {
    number: 2,
    title: 'Add the MCP server',
    description: 'This registers FLUX as a remote MCP server in your Codex config.',
    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: 'windsurf',
  label: 'Windsurf',
  steps: [{
    number: 1,
    title: 'Open the Windsurf MCP config',
    description: 'Edit <code>~/.codeium/windsurf/mcp_config.json</code>, or open <strong>Cascade → Plugins → Manage plugins → View raw config</strong>.',
    button: {
      label: 'Windsurf MCP docs',
      href: 'https://docs.windsurf.com/windsurf/cascade/mcp'
    }
  }, {
    number: 2,
    title: 'Add the FLUX server',
    description: 'Drop the JSON below into your config. Note that Windsurf uses <code>serverUrl</code>, not <code>url</code>. OAuth is handled automatically.'
  }, {
    number: 3,
    title: 'Reload plugins',
    description: 'Refresh plugins from the Cascade panel. The first FLUX call opens a browser for sign-in.'
  }],
  configCaption: 'Windsurf MCP config — paste this into ~/.codeium/windsurf/mcp_config.json:',
  configCode: `{
  "mcpServers": {
    "FLUX": {
      "serverUrl": "https://mcp.bfl.ai"
    }
  }
}`
}, {
  id: 'mcp-remote',
  label: 'Other MCP clients',
  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"]
    }
  }
}`
}];

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

## Connect FLUX

<a id="setup-instructions" />

Most modern MCP clients can connect to `https://mcp.bfl.ai` directly and handle the OAuth flow on their own — pick your client from the tabs below.

For stdio-only or OAuth-incompatible clients (for example Hermes), use the `mcp-remote` fallback tab. It runs locally, handles the browser OAuth flow, refreshes tokens for you, and exposes FLUX as a normal stdio server.

<SetupSteps tabs={setupTabs} />

<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>
</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. |
    | `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.                                                                                                                  |

    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 and per-model reference-image limits are also exposed as the `bfl://models` MCP resource.
  </Accordion>
</AccordionGroup>

## 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.
  </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.

***

## Agent Skills

MCP and Agent Skills solve different problems:

|                  | MCP                                                                | Agent Skills                                                    |
| ---------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- |
| **What it does** | Generates, edits, varies, and browses images directly in chat      | Teaches your coding agent how to write FLUX API code            |
| **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-best-practices@bfl-skills
    ```
  </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-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.

### 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)
