0

Widget State and LLM Context

awesome-llm-appsreferences

Manage persistent state, ephemeral UI state, and LLM interaction from widgets.

Sign in to save downloads to your library and vote.

Preview

Widget State and LLM Context

Manage persistent state, ephemeral UI state, and LLM interaction from widgets.

Decision Guide

NeedUseVisible to LLM?Persists?
Persist data across widget reopensstate + setState from useWidgetYesYes
Ephemeral UI only (hover, animation, local form)React useStateNoNo
Trigger LLM response from widgetsendFollowUpMessage from useWidgetN/AN/A
Call another tool from widgetcallTool from useWidgetN/AN/A

Persistent State: state + setState

State from useWidget() persists across widget reopens and is visible to the LLM.

import { useWidget } from "mcp-use/react";

function TodoList() {
  const { state, setState } = useWidget();

  const tasks = state?.tasks || [];

  const addTask = async (title: string) => {
    await setState((prev) => ({
      ...prev,
      tasks: [
        ...(prev?.tasks || []),
        { id: Date.now(), title, completed: false },
      ],
    }));
  };

  const toggleTask = async (id: number) => {
    await setState((prev) => ({
      ...prev,
      tasks: prev.tasks.map((t: any) =>
        t.id === id ? { ...t, completed: !t.completed } : t,
      ),
    }));
  };

  return (
    <div>
      {tasks.map((t: any) => (
        <div key={t.id} onClick={() => toggleTask(t.id)}>
          {t.completed ? "✓" : "○"} {t.title}
        </div>
      ))}
    </div>
  );
}

Why state: Tasks and progress survive widget close/reopen. The LLM can read the state to answer "how many tasks are left?"

Convenience Hook: useWidgetState

For simpler access:

import { useWidgetState } from "mcp-use/react";

function Counter() {
  const [state, setState] = useWidgetState({ count: 0 });

  return (
    <button
      onClick={() => setState((prev) => ({ count: (prev?.count || 0) + 1 }))}
    >
      Count: {state?.count || 0}
    </button>
  );
}

Ephemeral State: React useState

Use React useState for UI-only state that doesn't need to persist or be visible to the LLM.

import { useState } from "react";

function ProductCard({ product }) {
  // Ephemeral: hover state resets on reopen, LLM doesn't need it
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{ transform: isHovered ? "scale(1.02)" : "scale(1)" }}
    >
      {product.name}
    </div>
  );
}

Why useState: Hover state is purely visual, resets on reopen, and the LLM never needs to know about it.

Common Mistake

// DON'T: useState loses data on close, LLM can't see it
const [selected, setSelected] = useState(null);

// DO: state persists, LLM sees selections
const { state, setState } = useWidget();
const selected = state?.selected;

Calling Other Tools: callTool

Widgets can call any tool registered on the server:

import { useWidget } from "mcp-use/react";
import { useState } from "react";

function SearchWidget() {
  const { callTool } = useWidget();
  const [result, setResult] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSearch = async (query: string) => {
    setLoading(true);
    try {
      const response = await callTool("search-items", { query });
      setResult(response.content);
    } catch (err) {
      console.error("Search failed:", err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button onClick={() => handleSearch("shoes")} disabled={loading}>
        {loading ? "Searching..." : "Search"}
      </button>
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </div>
  );
}

Triggering LLM Response: sendFollowUpMessage

Trigger the LLM to respond from a widget interaction:

import { useWidget } from "mcp-use/react";

function AnalyzeButton() {
  const { sendFollowUpMessage } = useWidget();

  return (
    <button
      onClick={() =>
        sendFollowUpMessage(
          "Compare the top 3 results and recommend the best one based on price and reviews.",
        )
      }
    >
      Ask AI to Analyze
    </button>
  );
}

Use this when:

  • User clicks "Find best option" and the LLM should reason about the data
  • User completes a selection and the LLM should provide recommendations
  • Widget needs the LLM to take action based on current state

Opening External URLs: openExternal

Redirect users to external sites:

import { useWidget } from "mcp-use/react";

function CheckoutButton({ checkoutUrl }) {
  const { openExternal } = useWidget();

  return (
    <button onClick={() => openExternal(checkoutUrl)}>
      Proceed to Payment
    </button>
  );
}

Combined Example

Todo list with persistent tasks, ephemeral viewing state, and LLM interaction:

import { McpUseProvider, useWidget } from "mcp-use/react";
import { useState } from "react";

export default function TodoWidget() {
  const { props, isPending, state, setState, sendFollowUpMessage } =
    useWidget();

  // PERSISTENT: Tasks survive widget close/reopen, LLM sees them
  const tasks = state?.tasks || props.tasks || [];

  // EPHEMERAL: Which task is being viewed (resets on reopen)
  const [viewing, setViewing] = useState<string | null>(null);

  if (isPending)
    return (
      <McpUseProvider autoSize>
        <div>Loading...</div>
      </McpUseProvider>
    );

  const toggleTask = async (id: string) => {
    await setState((prev: any) => ({
      ...prev,
      tasks: (prev?.tasks || tasks).map((t: any) =>
        t.id === id ? { ...t, completed: !t.completed } : t,
      ),
    }));
  };

  const remaining = tasks.filter((t: any) => !t.completed).length;

  return (
    <McpUseProvider autoSize>
      <div style={{ padding: 16 }}>
        <h2>{remaining} tasks remaining</h2>
        {tasks.map((t: any) => (
          <div
            key={t.id}
            onClick={() => setViewing(t.id)}
            style={{
              padding: 8,
              cursor: "pointer",
              opacity: t.completed ? 0.5 : 1,
            }}
          >
            <input
              type="checkbox"
              checked={t.completed}
              onChange={() => toggleTask(t.id)}
            />
            {t.title}
          </div>
        ))}
        <button
          onClick={() =>
            sendFollowUpMessage(
              `I have ${remaining} tasks left. Help me prioritize them.`,
            )
          }
        >
          Ask AI to Prioritize
        </button>
      </div>
    </McpUseProvider>
  );
}

Why each?

WhatAPIWhy
tasksstate + setStatePersists. Progress survives reopen. LLM sees completion status.
viewinguseStateEphemeral. Current focus resets on reopen.
"Help me prioritize"sendFollowUpMessageTriggers LLM reasoning based on current tasks.

Ingestion metadata

Source catalog
awesome-llm-apps
File path
generative_ui_agents/ai-mcp-app-builder/apps/mcp-use-server/.agent/skills/chatgpt-app-builder/references/state-and-context.md
Last refreshed
10/24/2026, 3:00:14 AM (5h ago)
Refresh schedule
Daily · 03:00 UTC
Dedupe status
Unique · deduped by (source, url)