Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs-staging.skybridge.tech/llms.txt

Use this file to discover all available pages before exploring further.

How to get data into your view and fetch more when the user interacts.

Two Patterns

PatternHookWhen to use
Initial hydrationuseToolInfoData passed when view loads
User-triggereduseCallToolUser clicks button, needs more data

Initial Hydration with useToolInfo

When the host calls your view’s tool, the server returns structuredContent. Access it with useToolInfo:
import { useToolInfo } from "skybridge/web";

type FlightData = {
  flights: Array<{ id: string; name: string; price: number }>;
};

export function FlightView() {
  const { isSuccess, output } = useToolInfo<FlightData>();

  if (!isSuccess) {
    return <div>Loading...</div>;
  }

  const { flights } = output.structuredContent;

  return (
    <ul>
      {flights.map(flight => (
        <li key={flight.id}>{flight.name} - ${flight.price}</li>
      ))}
    </ul>
  );
}

Status Flags

const { isIdle, isPending, isSuccess, isError, output, error } = useToolInfo();
FlagMeaning
isIdleView hasn’t received data yet
isPendingTool is still executing
isSuccessData is available in output
isErrorTool failed, check error

User-Triggered Fetching with useCallTool

When the user clicks something and you need more data:
Tool Accessibility: In Apps SDK (ChatGPT), tools must have _meta["openai/widgetAccessible"] set to true to be callable from views. In MCP Apps, all tools are accessible by default. See useCallTool API for details.
import { useCallTool } from "skybridge/web";

export function FlightView() {
  const { callTool, isPending, data, isError, error } = useCallTool("get_flight_details");

  const handleViewDetails = (flightId: string) => {
    callTool({ flightId });
  };

  return (
    <div>
      <button onClick={() => handleViewDetails("AF123")} disabled={isPending}>
        {isPending ? "Loading..." : "View Details"}
      </button>

      {isError && <p>Error: {String(error)}</p>}
      {data && <pre>{JSON.stringify(data.structuredContent, null, 2)}</pre>}
    </div>
  );
}

With Callbacks

const { callTool } = useCallTool("book_hotel");

const handleBooking = () => {
  callTool(
    { hotelId: "123" },
    {
      onSuccess: (response) => {
        console.log("Booking confirmed:", response.structuredContent);
        setBookingComplete(true);
      },
      onError: (error) => {
        console.error("Booking failed:", error);
        setShowErrorModal(true);
      },
      onSettled: () => {
        // Runs after success or error
        setSubmitting(false);
      },
    }
  );
};

With Async/Await

const { callToolAsync, isPending } = useCallTool("check_availability");

const handleCheck = async (date: string) => {
  try {
    const result = await callToolAsync({ date });
    if (result.structuredContent.available) {
      setStep("payment");
    } else {
      setShowUnavailableMessage(true);
    }
  } catch (error) {
    setError(error);
  }
};

Type Safety with generateHelpers

Use generateHelpers for autocomplete and type inference. See generateHelpers for setup.
// Import from your typed helpers file, not skybridge/web
import { useCallTool } from "../skybridge";

const { callTool, data } = useCallTool("search-hotels");
//                                       ^ autocomplete for tool names
callTool({ city: "Paris", checkIn: "2025-12-15" });
//         ^ type-checked inputs

Common Patterns

Loading State

function ProductView() {
  const { isPending, data } = useCallTool("get_product");

  if (isPending) {
    return <Skeleton />;
  }

  if (!data) {
    return <EmptyState />;
  }

  return <ProductDetails product={data.structuredContent} />;
}

Refetching

function StockView() {
  const { callTool, data, isPending } = useCallTool("get_stock_price");
  const [lastFetch, setLastFetch] = useState<Date | null>(null);

  const refresh = () => {
    callTool({ symbol: "AAPL" }, {
      onSuccess: () => setLastFetch(new Date()),
    });
  };

  return (
    <div>
      <button onClick={refresh} disabled={isPending}>
        {isPending ? "Refreshing..." : "Refresh"}
      </button>
      {lastFetch && <p>Last updated: {lastFetch.toLocaleTimeString()}</p>}
      {data && <StockPrice price={data.structuredContent.price} />}
    </div>
  );
}

Chained Calls

function BookingView() {
  const { callToolAsync: checkAvailability } = useCallTool("check_availability");
  const { callToolAsync: createBooking } = useCallTool("create_booking");

  const handleBook = async (roomId: string, dates: DateRange) => {
    const availability = await checkAvailability({ roomId, ...dates });

    if (!availability.structuredContent.available) {
      setError("Room not available for these dates");
      return;
    }

    const booking = await createBooking({
      roomId,
      ...dates,
      priceId: availability.structuredContent.priceId,
    });

    setConfirmation(booking.structuredContent);
  };
}

Anti-patterns

Don’t fetch on mount

// Bad: Fetches data on every mount
function BadView() {
  const { callTool } = useCallTool("get_data");

  useEffect(() => {
    callTool({}); // Don't do this!
  }, []);
}

// Good: Use initial data from structuredContent
function GoodView() {
  const { output } = useToolInfo();
  // Data is already here from the tool call
}

Don’t ignore loading states

// Bad: Assumes data is always present
function BadView() {
  const { data } = useCallTool("search");
  return <div>{data.structuredContent.results.length}</div>; // Crashes!
}

// Good: Handle all states
function GoodView() {
  const { data, isPending, isError } = useCallTool("search");

  if (isPending) return <Loading />;
  if (isError) return <Error />;
  if (!data) return null;

  return <div>{data.structuredContent.results.length}</div>;
}