ChatGPT apps make a familiar product mistake tempting again: ship the interface first, then let the tool contract catch up later. I would do it in the opposite order. As of August 2026, the Apps SDK is still described as preview tooling, but app submissions are open, the examples repo is mature enough to study, and the underlying Model Context Protocol pieces are specific enough to design against. The useful work is not picking a prettier widget. It is deciding what the model can know, what the widget can mutate, and what the user must explicitly approve.
The contract I would write first
Before I build a ChatGPT app, I want a one-page contract with four rows:
| Layer | Decision to write down |
|---|---|
| Tool schema | What inputs the model may send, and which calls are read-only or mutating |
| Tool result | What structured content the model receives, separate from UI-only metadata |
| Widget state | What the component can remember between turns, and what remains server-side |
| User approval | Which moments require connect, decline, cancel, or a fresh confirmation |
That is enough. A larger architecture document usually hides uncertainty instead of reducing it. The contract should be boring because the runtime is already doing enough interesting work: ChatGPT, an MCP server, a tool result, a rendered component, and often a signed-in backend account.
OpenAI's Apps SDK material frames apps as chat logic plus interface, built on MCP and connected to an existing backend. The examples repo shows the concrete shape: tools advertise JSON Schema contracts, return structured content, and attach widget resources through metadata so ChatGPT can render an interface alongside the conversation. That means the tool result is not just an API response. It is a negotiation between three readers: the model, the widget, and the human.
Keep tool output boring
The laziest production rule is to make the tool output useful without the widget. If the component fails to render, the assistant should still have enough structured content to answer accurately. If the model ignores the component, the widget should still receive enough metadata to show the right state. If the user refreshes the conversation, the server should still know the durable source of truth.
I would avoid returning a blob of presentational text as the main tool result. Return domain objects with stable identifiers, labels, statuses, and next actions. Keep prose in the assistant response and layout in the widget. The Apps SDK examples point in this direction by separating structured payloads from UI resources, and by using widget session metadata to keep shopping-cart state aligned across tool calls.
This also makes review easier. A pull request reviewer can ask, "Can this tool result be consumed by a model without the widget?" If the answer is no, the app is probably too coupled to a single rendering path.
Treat elicitation as a UX boundary
The MCP 2025-06-18 spec added elicitation so servers can request user input through the client during a workflow. The important detail is the constraint: the requested schema is a flat object with primitive values, and responses distinguish accept, decline, and cancel. The spec also says servers must not use elicitation for sensitive information.
That makes elicitation a good fit for small confirmations and missing non-sensitive fields. It is a poor fit for passwords, payment secrets, or anything that should happen in a dedicated account flow. In a ChatGPT app, I would keep this rule: elicitation fills a gap, authentication grants access, and a mutating tool call performs the action. Mixing those three creates audit pain.
This is where the earlier remote MCP governance checklist still matters. Governance is the outer boundary. Elicitation is the user-interface checkpoint inside it.
Review widget state like product data
The Apps SDK examples expose the full widget surface through patterns such as reading host state, writing widget state, calling another MCP tool from the component, opening external links, and requesting display changes. That power is useful, but it needs the same review discipline as backend state.
I separate widget state into three buckets:
- Cosmetic state, such as selected tab or expanded row. Store it locally.
- Conversation state, such as a draft cart or selected itinerary. Mirror it through widget state, but make the next tool call validate it.
- Business state, such as purchases, bookings, saved records, or account settings. Persist it server-side only after an explicit mutating action.
The middle bucket is where mistakes happen. A widget can make a draft feel more final than it is. A model can refer to stale widget state if the server does not validate identifiers again. A user can assume a visible selection was saved when it was only local. The fix is small: every mutating tool should re-read the durable record, validate the submitted identifiers, and return the new state in the tool result.
This is also an observability problem. In my production agent telemetry contract, I argued for tracing decisions, tool calls, and handoffs. Apps need the same minimum trace: tool name, schema version, widget session id when present, mutating flag, user approval action, and durable record id.
A launch checklist I would actually use
Before publishing a ChatGPT app, I would run this checklist:
- The app works in text-only form for its core flow.
- Every tool has a stable input schema and a documented output shape.
- Read-only tools are marked and reviewed separately from mutating tools.
- Widget metadata is treated as rendering support, not the source of truth.
- Elicitation is limited to non-sensitive, low-friction fields.
- Decline and cancel paths return usable next steps, not dead ends.
- Mutating calls validate server-side state immediately before writing.
- External links disclose where the user is going.
- The privacy policy matches the actual data the app requests.
- Traces let support reconstruct one failed action without reading private content.
None of this requires a framework beyond the SDK patterns already shown in the examples. The hard part is resisting the urge to make the widget the product boundary. The product boundary is the contract.
The forward-looking bet
The strongest ChatGPT apps will not feel like tiny websites embedded in a chat. They will feel like narrow tools that know when a conversation is enough, when a component is clearer, and when the user needs a real approval step. I would spend less time polishing the first component and more time making the tool result, widget state, and approval path boring enough that each can fail independently without corrupting the workflow.