EggAPI/docs / dynamic-ui
sign in

Dynamic UI

Agents compose a form page from a short JSON description and use one session in the conversation (MCP Apps), a browser, the CLI, or plain text

Dynamic UI lets an agent stop asking for parameters one by one and instead compose a page for the user's goal from pre-built components: for "make an ad image" the page shows the product image, brand, selling points, placement, style, model, cost estimate, and a generate button. The agent only writes a few KB of JSON; the renderer is compiled ahead of time, so opening a page generates no code and calls no model.

  • One set of components: ad images, product shots, image-to-video pages and anything else are composed from the same components and rendered by the same renderer. They are not fixed templates.
  • One session: MCP, the CLI and the browser read and write the same server-side session, so the agent can read what the user entered and the task results.
  • No accidental charges: creating or updating a page, choosing a model and editing fields never generate anything. Paid tasks start only when the user clicks a generate button (or the agent calls run_ui_action after the user explicitly agreed), and one click is never charged twice.

Four ways in

Client capabilityEntryWhat the user sees
Supports MCP AppsThe MCP tool result carries the UIThe page inline in the conversation
MCP without MCP Appsbrowser_url in the tool resultThe same page in a browser
No MCP, can run commandsThe eggapi CLIThe agent creates the page by command and shares the link
No browser, SSH, headlessget_ui_state lists fields, options and missing valuesThe agent asks in chat, then calls set_ui_values and run_ui_action

The browser page is a full entry point with the same features as the inline UI, including image upload. Tool results always carry text, structured data and the browser link, so clients without UI support can complete the whole flow.

Client support

As of 2026-10-28. "Docs only" means based on the client's official documentation, not run in that client; "Tested" means covered by this repository's automated tests.

ClientMCP connectionUI in the conversationStatus
Claude CodeHTTP or stdioIts docs do not say the terminal renders MCP Apps; use the browser linkSetup docs only, not run in the client
Codex CLIHTTP or stdioIts docs do not mention MCP Apps; use the browser linkSetup docs only, not run
Gemini CLIHTTP or stdioIts docs describe text, image and audio content only; use the browser linkSetup docs only, not run
OpenCodeHTTP or stdioIts docs do not mention MCP Apps; use the browser linkSetup docs only, not run
Claude / Claude Desktop, ChatGPT, VS Code GitHub Copilot, Goose, ...HTTPListed as MCP Apps hosts in the MCP Apps docsNot run in these hosts
Official Go MCP SDK clientHTTP—Tested: tools, resource, capability detection, error structure
Simulated MCP Apps hostpostMessageYesTested: handshake, theme, narrow width, keyboard, submit and results (a simulated host is not a real host)

Setup

Create an API key in the dashboard and export it in the terminal that starts your agent:

bash
export EGGAPI_API_KEY="egg_..."

The remote MCP endpoint is https://api.eggapi.ai/mcp (Streamable HTTP, Authorization: Bearer <key>). Clients already connected to EggAPI MCP need no change: the new tools appear automatically. Clients without HTTP MCP can use eggapi mcp over stdio, which forwards messages to the same endpoint unchanged.

Claude Code

bash
claude mcp add --transport http eggapi https://api.eggapi.ai/mcp \
  --header "Authorization: Bearer $EGGAPI_API_KEY"

Over stdio:

bash
claude mcp add --env EGGAPI_API_KEY=$EGGAPI_API_KEY eggapi -- eggapi mcp

Codex CLI

bash
codex mcp add eggapi --url https://api.eggapi.ai/mcp --bearer-token-env-var EGGAPI_API_KEY

Or in ~/.codex/config.toml:

toml
[mcp_servers.eggapi]
url = "https://api.eggapi.ai/mcp"
bearer_token_env_var = "EGGAPI_API_KEY"

Gemini CLI

bash
gemini mcp add --transport http --header "Authorization: Bearer $EGGAPI_API_KEY" eggapi https://api.eggapi.ai/mcp

Or in ~/.gemini/settings.json (httpUrl is Streamable HTTP; url means SSE):

json
{
  "mcpServers": {
    "eggapi": {
      "httpUrl": "https://api.eggapi.ai/mcp",
      "headers": { "Authorization": "Bearer <your API key>" }
    }
  }
}

OpenCode

In the project's opencode.json or ~/.config/opencode/opencode.json:

json
{
  "mcp": {
    "eggapi": {
      "type": "remote",
      "url": "https://api.eggapi.ai/mcp",
      "headers": { "Authorization": "Bearer {env:EGGAPI_API_KEY}" }
    }
  }
}

For stdio, use { "type": "local", "command": ["eggapi", "mcp"], "environment": { "EGGAPI_API_KEY": "egg_..." } } instead.

Clients that support MCP Apps

Add EggAPI as described in Connect AI agents. Clients that only take a URL use the personal link from the dashboard, https://api.eggapi.ai/mcp/k/<your key>.

Optional: skill

eggapi-dynamic-ui is a thin skill that only tells the agent when to use dynamic UI and in which order to call the tools. Everything works without it; install it like the workflow skills.

Agent workflow

  1. Call get_ui_catalog once for the component contract. Pass example (ad_image, product_main_image, image_to_video) for a reference page.
  2. Choose the fewest suitable components, write the page description and call render_ui. Use values to pre-fill what the conversation already tells you.
  3. Give the user browser_url. Clients that support MCP Apps also show the page inline.
  4. When the user says they are done, call get_ui_state for their values, missing fields, cost estimate and task results.
  5. When the user wants the page changed ("add a copy field", "switch to a video ad", "hide the model choice"), call update_ui with the complete new description and base_revision.
  6. Call run_ui_action only after the user explicitly agreed to the estimated cost, and pass the amount they agreed to as expected_cost_usd.

The agent never writes HTML, CSS or React, installs dependencies, starts servers or runs browser tests.

Page description (UI Schema 1.0)

json
{
  "schema_version": "1.0",
  "page": { "id": "image_to_video", "title": "Image to video" },
  "initial_values": { "aspect_ratio": "9:16", "duration": 5 },
  "actions": [
    {
      "id": "animate",
      "type": "generate",
      "params": {
        "model": { "field": "model" },
        "prompt": { "field": "motion" },
        "images": { "field": "first_frame" },
        "aspect_ratio": { "field": "aspect_ratio" },
        "resolution": { "field": "resolution" },
        "duration": { "field": "duration" }
      }
    }
  ],
  "root": {
    "id": "root",
    "type": "stack",
    "children": [
      { "id": "first_frame", "type": "image_input", "props": { "label": "First frame", "required": true } },
      { "id": "motion", "type": "textarea", "props": { "label": "Motion", "required": true } },
      { "id": "aspect_ratio", "type": "aspect_ratio_picker", "props": { "label": "Aspect ratio", "options": ["9:16", "16:9", "1:1"] } },
      { "id": "duration", "type": "number_input", "props": { "label": "Duration", "required": true, "min": 1, "max": 600, "unit": "s" } },
      { "id": "model", "type": "model_picker", "props": { "label": "Video model", "required": true, "kind": "video", "generation_types": ["image-to-video"] } },
      {
        "id": "resolution",
        "type": "select",
        "visible_when": { "field": "model", "op": "model_has_resolutions" },
        "props": { "label": "Resolution", "options_source": { "source": "model_resolutions", "model_field": "model" } }
      },
      { "id": "cost", "type": "cost_estimate", "props": { "action": "animate" } },
      { "id": "generate", "type": "generate_button", "props": { "action": "animate", "label": "Generate video", "confirm": true } },
      { "id": "status", "type": "task_status", "props": { "action": "animate" } },
      { "id": "preview", "type": "video_preview", "props": { "action": "animate" } }
    ]
  }
}
FieldMeaning
schema_versionRequired; only "1.0" is supported. Other versions return unsupported_schema_version.
pageid (lowercase letters, digits, underscores), title, optional description.
rootRoot of the component tree; must be a layout component.
initial_valuesStarting values keyed by field id, checked against each field's type.
actionsPre-registered actions and their parameter bindings, at most 8.

Every component node has a stable id, a type and props, and optionally visible_when and children. Input components edit the field named by field, which defaults to the id. Keep ids unchanged when updating a page and the user's input is preserved.

Limits: at most 64 KB of JSON, 150 components, 10 levels of nesting and 4 levels of condition nesting. A page description is data only: no HTML, scripts, code URLs or backend tool names, and components and actions can only reference names in the catalog.

Components

ComponentCategoryValueMain props
stacklayout—gap (sm/md/lg), direction (vertical/horizontal)
gridlayout—columns (1-4, one column on narrow screens), gap
sectionlayout—title, description
text_inputinputstringlabel, placeholder, help, required, max_length
textareainputstringas above, plus rows
selectinputstringoptions ([{value,label}]) or options_source, exactly one
segmented_controlinputstringoptions (2-6)
number_inputinputnumbermin, max, step, integer, unit
image_inputinputimage_listmax_images (1-8); upload in the browser, image links elsewhere
model_pickerbusinessstringkind (image/video), generation_types, show_price; models and prices come from the backend
aspect_ratio_pickerbusinessstringoptions (subset of 1:1, 3:4, 4:3, 9:16, 16:9, 21:9)
cost_estimatebusiness—action; live prices, same formula as the server's balance hold
generate_buttonbusiness—action, label, confirm; the only component that submits paid work
task_statusbusiness—action; queued/running/done/failed, elapsed time, actual cost, no invented progress percentage
image_gallerybusiness—action, columns
video_previewbusiness—action

The full prop types, defaults and ranges are whatever get_ui_catalog (or GET https://api.eggapi.ai/v1/ui/catalog) returns; it reads the same registry the server validates with.

Conditions

visible_when shows or hides a component. Hidden fields are neither required nor submitted.

json
{ "field": "background", "op": "eq", "value": "custom" }
{ "all": [ { "field": "mode", "op": "eq", "value": "video" }, { "field": "model", "op": "model_requires_image" } ] }
OperatorValueMeaning
eq, neqscalarequals, differs
in, not_inlist of scalarsone of, none of
empty, not_emptynonefilled in or not
gt, gte, lt, ltenumbernumber_input fields only
model_kind_eqimage or videokind of the selected model
model_generation_type_inlist of generation typesgeneration type of the selected model
model_requires_imagenonethe selected model needs an input image
model_has_resolutionsnonethe selected model is priced per resolution

Conditions combine only with all, any and not; there are no expressions or scripts.

Data sources

options_source of a select currently supports {"source": "model_resolutions", "model_field": "<model field>"}: the resolutions the selected model is priced for, cheapest first. When the model declares none, the list is empty and the model's default is used.

Actions

The only action type is generate; it creates an image or video task depending on the selected model. Each parameter takes one binding:

BindingExampleMeaning
field{"field": "model"}a field's value; the component must be compatible
value{"value": "bytedance/seedance-2.5/image-to-video"}a constant, e.g. a fixed model after "hide the model choice"
template{"template": "Brand: {brand}\nSelling points: {selling_points}"}combines fields into text; a line with an empty placeholder is dropped; {{ and }} are literal braces
ParamRequiredMeaning
modelyesmodel_picker field or constant
promptyestext field, template or constant
negative_promptnoas above
imagesnoimage_input field; references for image models, first frame (one image) for video models
aspect_rationoratio picker, select or constant (default 16:9)
resolutionnomust be a resolution the model is priced for
num_outputsno1-16, image models only
durationfor video1-600 s, billed per second; the backend publishes no per-model range, so none is assumed

On submit the server checks the inputs against the selected model: image-to-* models need an image, text-to-* models refuse images, video models take one first frame, and the resolution must be priced.

Errors

Every validation problem comes with a JSON pointer path, a code and a hint, so an agent can fix it directly:

json
{
  "error": {
    "code": "SCHEMA_INVALID",
    "issues": [
      { "path": "/root/children/2/type", "code": "unknown_component", "message": "unknown component \"html\"", "hint": "allowed: stack, grid, ..." }
    ]
  }
}
CodeMeaning
SCHEMA_INVALIDinvalid page description; issues lists each problem
REVISION_CONFLICTthe page changed since you read it; the error carries the current session
ACTION_NOT_READYrequired values missing, not valid for the model, or the estimate changed (estimate_changed); issues points at the fields
TOO_MANY_SESSIONSan account keeps at most 200 unexpired sessions; reuse a page with update_ui
INSUFFICIENT_BALANCE, CONTENT_BLOCKEDnot enough balance, or blocked by the NSFW filter; nothing is charged
NOT_FOUNDthe session does not exist, expired, or belongs to another account

Sessions and updates

The server keeps one session per page: session_id, schema_version, revision (bumped on every change), schema_revision (the revision of the last page description change), fingerprint (changes with the revision and with live model prices), the page description, values, submissions, status and expiry. A session belongs to the EggAPI account that created it and expires after 7 days without changes; expired sessions are deleted the next time that account creates a page, and an account keeps at most 200 unexpired sessions.

update_ui rules:

  • base_revision must equal the current schema_revision, otherwise you get REVISION_CONFLICT and nothing is overwritten. The user editing values meanwhile is not a conflict.
  • A value is kept when its field id and value type are unchanged and it is still valid for the new field (reconcile.preserved).
  • When the type changed or the value is no longer valid (a removed option, an image model after switching to a video picker) it is reset (reconcile.reset); removed fields are listed with their previous value in reconcile.removed. Stale values are never submitted silently.
  • An open page picks up the new version within seconds and updates in place: text being typed and focus are kept.

Browser page

browser_url looks like https://eggapi.ai/ui/<session_id>. It contains no API key; opening it requires signing in to the same EggAPI account, so it works on any device. Links created by agents on a laptop, a remote server, over SSH or in a container all point at the EggAPI website, not at the agent machine's localhost. Without a GUI, copy the link to any browser; the CLI never opens one.

The page saves input automatically and checks for updates every few seconds; while a task runs it refreshes at the task API's polling limits (5 s for images, 10 s for videos), without the agent polling.

When the user submits in the browser, the result is written to the session, but a browser cannot wake an agent in a terminal. Tell the agent you are done and it reads the result with get_ui_state. MCP Apps hosts that support ui/update-model-context also receive the latest state in the agent's context.

CLI

eggapi is a standard-library-only command-line tool that shares the server's validation, sessions and account scoping with MCP. There are no prebuilt releases yet; build it from source with repository access (Go 1.23+):

bash
git clone https://github.com/danaigc/eggapi && cd eggapi/backend
go build -o ~/.local/bin/eggapi ./cmd/eggapi
CommandDoes
eggapi ui catalog --jsonthe component contract; --example ad_image returns a reference page
eggapi ui create --schema-file page.json --jsoncreate a page; - reads stdin
eggapi ui update <id> --schema-file page.json --base-revision N --jsonupdate a page
eggapi ui state <id> --jsonvalues, missing fields, actions and task results; --full adds the page description
eggapi ui set <id> --values '{"brand":"Acme"}' --jsonset field values
eggapi ui run <id> <action> --schema-revision N --expected-cost USD --submission-id S --jsonrun the paid action; refused if the estimate differs from --expected-cost; retry with the same --submission-id and it is never charged twice (network or server errors return that id)
eggapi ui wait <id> --jsonwait for the latest task at 5/10 s intervals
eggapi mcpMCP server over stdio

With --json, stdout holds exactly one JSON document and logs go to stderr. Errors print {"error": {"code", "message", "http_status", "details"}}. Exit codes: 0 ok, 2 usage, 3 rejected (validation, conflict, balance, ...), 4 authentication, 5 network or server, 6 wait timeout. Credentials come from EGGAPI_API_KEY; EGGAPI_API_BASE overrides the API URL.

Without a GUI

Without a browser, the agent completes the same flow in text:

  1. Create the page with render_ui (or reuse one).
  2. get_ui_state returns each field's label, whether it is required, missing and visible, its value, and the options it currently allows (models, resolutions, ratios, ...).
  3. Ask the user in chat and write the answers with set_ui_values; rejected values come back with reasons.
  4. Show the user actions[].estimated_cost_usd and call run_ui_action only after they agree, with expected_cost_usd set to the amount they agreed to.
  5. Read the task status, output URLs and actual cost later with get_ui_state.

Billing and security

  • The estimate uses the same formula as the balance hold taken when a task is created: per image or per second of video, at the model's resolution price. On submit the server recomputes it from the actual parameters; the task is settled at its real cost and refunded if it fails.
  • Each click carries a submission_id. The server deduplicates on it and derives a fixed task id from it, so retries and double clicks never create a second task. When the outcome is unknown (network failure, gateway error), the page reuses the same id on the next click and only looks up that submission.
  • A click submits every field value shown on screen together with the estimate on the button. If the server's estimate differs (prices changed, or values were edited elsewhere), or the submit would invalidate a chosen value (a resolution the new model does not offer), the run is refused with the reason instead of charging an amount the user did not see.
  • Once generation starts it is detached from the request: closing the page or a CLI timeout cannot lose the record of a submission whose balance is held. A submission the server stopped in the middle of is completed from the task record the next time the session is read.
  • Generations submitted from the browser page always use the NSFW filter, like the playground; submissions with an API key (MCP, CLI) follow the key's request.
  • The API key stays in the agent's client configuration and on the server; it never appears in page descriptions, the browser page or links. The MCP Apps UI calls tools through its host; the browser page uses the website sign-in.

Limitations

  • The four CLIs do not show the inline UI in the terminal (their docs say nothing about it); use the browser link or the text flow.
  • The MCP Apps UI runs in the host's sandbox: it cannot upload local files (image links only; upload in the browser page), and output images or videos from domains not listed in the server's UI_MCP_RESOURCE_DOMAINS appear as links instead of previews.
  • The MCP server is stateless and cannot see capabilities declared at initialize. When a client declares MCP Apps support in each request's _meta, results say ui_mode: "embedded"; otherwise they say link and lead with the browser link, and hosts that support MCP Apps still render the UI.
  • Submitting in the browser does not notify the agent; the user tells it in the conversation.
  • Aspect ratios are the six documented ones because the backend does not publish per-model support; video duration is only checked against the API's 1-600 s.
  • There is no "rewrite copy / improve prompt" action yet: the agent writes copy in the conversation and puts it on the page through values or update_ui.
connected
mcp https://api.eggapi.ai/mcp
models ...
Claude Code · Codex · MCP
█